NestJS raw body로 웹훅 서명 검증하기

NestJS에서는 NestFactory.create에 rawBody: true를 넘기고, Buffer인 req.rawBody로 웹훅 서명을 검증하세요. 100kb JSON 한도도 함께 올리세요.

읽는 시간 5분Sume
전체 글

NestJS에서 raw body(원본 본문)를 얻으려면 NestFactory.create()에 { rawBody: true }를 넘기고, 핸들러의 요청 타입을 RawBodyRequest<Request>로 지정하세요. 그러면 req.rawBody는 도착한 그대로의 본문을 담은 Buffer가 됩니다. 웹훅 서명 검증에 필요한 것이 바로 이 버퍼입니다. Sume 웹훅이라면 @sume-com/sdk의 verifyWebhook으로 검증하고, JSON은 검증한 바이트에서만 파싱하세요.

NestJS 관련 내용은 NestJS의 Raw body, 컨트롤러, 예외 필터 문서에서, Sume 관련 내용은 웹훅 검증, Run 웹훅 (영문), 웹훅 (영문)에서 가져왔습니다. 모두 2026-09-27에 확인했습니다. Sume에는 NestJS용 모듈이 없으며, 수신기는 일반 컨트롤러입니다. 순수 Express에서는 같은 문제를 라우트 미들웨어로 해결하며, 그 방법은 Express에서 raw body로 웹훅 서명 검증하기에서 다룹니다.

req.body로 서명을 검증하면 왜 실패하나요?

Nest가 이미 본문을 파싱했기 때문입니다. Sume는 모든 전달을 <timestamp>.<raw_body>에 대한 HMAC-SHA256으로 서명하며, 파싱했다가 다시 직렬화한 객체는 검증되지 않습니다. 키 순서와 공백도 서명 대상의 일부이기 때문입니다. NestJS 문서도 raw body를 읽어야 하는 가장 흔한 이유 중 하나로 웹훅 서명 검증을 꼽습니다. HMAC에는 파싱하지 않은 바이트가 필요하기 때문입니다.

raw body는 Nest에 내장된 전역 body parser가 켜져 있을 때만 존재하므로, 앱을 만들 때 bodyParser: false를 넘기지 마세요.

NestJS에서 rawBody는 어떻게 켜나요?

앱을 만드는 곳에서 설정하고, 같은 곳에서 JSON 한도도 올리세요. Nest의 기본 플랫폼인 Express에서 body parser 한도의 기본값은 100kb이며, useBodyParser()는 rawBody 옵션을 따릅니다.

NestJS의 Raw body 문서와 Sume의 Run 웹훅 (영문) 페이지 기준, 2026-09-27 확인.
설정NestJS 동작Sume 웹훅의 경우
NestFactory.create()의 rawBody: true파싱하지 않은 본문을 보관. 내장 body parser가 필요필수. 서명은 원본 바이트를 대상으로 함
RawBodyRequest<Request>req.rawBody를 Buffer로 노출Buffer를 그대로 verifyWebhook에 전달
app.useBodyParser("json", { limit })Express 기본값: 100kb1 MiB보다 크게 설정. Sume Run 웹훅의 본문 한도는 1,048,576바이트
// main.ts
import { NestFactory } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import { AppModule } from "./app.module.js";

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    rawBody: true, // keeps req.rawBody; never pass bodyParser: false
  });
  app.useBodyParser("json", { limit: "2mb" }); // Express default is 100kb
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

NestJS 컨트롤러에서 Sume 웹훅은 어떻게 검증하나요?

@Req()로 요청을 주입하고, req.rawBody를 검증한 다음에야 파싱하세요. verifyWebhook은 본문을 문자열, ArrayBuffer, 타입 배열로 받으므로 Buffer를 그대로 넘길 수 있고, Node의 req.headers 같은 일반 객체에서 헤더를 읽습니다. 이 함수는 async이고, 예외를 던지는 대신 false를 반환하며, 상수 시간으로 비교하고, 기본적으로 300초의 재전송 허용 시간을 적용합니다. Nest는 @HttpCode(...)를 지정하지 않으면 POST에 201로 응답하는데, 어떤 2xx든 전달된 것으로 인정됩니다.

  • 시크릿은 대시보드의 웹훅 탭에서 확인하거나 account:read가 있는 키로 GET /v1/webhooks/signing-secret을 호출해 읽고, SUME_COM_WEBHOOK_SIGNING_SECRET으로 저장하세요. 아래 모듈은 이 값이 없으면 시작을 거부하므로, 빈 시크릿으로 검증이 통과하는 일은 없습니다.
  • 시크릿을 교체한 뒤 24시간 동안 서명 헤더에는 쉼표로 구분된 sume-v1= 항목이 두 개 실립니다. @sume-com/sdk 0.2.0의 verifyWebhook은 둘 중 어느 쪽이든 받아들이지만, 헤더 전체를 비교하는 검증은 그 기간 동안 실패합니다.
import {
  BadRequestException, Controller, HttpCode, Post, Req,
  UnauthorizedException, type RawBodyRequest,
} from "@nestjs/common";
import type { Request } from "express";
import { verifyWebhook } from "@sume-com/sdk";

const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is not set");

@Controller("hooks")
export class SumeWebhookController {
  @Post("sume")
  @HttpCode(204)
  async receive(@Req() req: RawBodyRequest<Request>) {
    if (!req.rawBody) throw new BadRequestException("no raw body");
    const ok = await verifyWebhook({ body: req.rawBody, headers: req.headers, secret });
    if (!ok) throw new UnauthorizedException("bad signature");
    const event = JSON.parse(req.rawBody.toString("utf8")); // the verified bytes
    await recordOnce(event); // your insert-or-ignore on request_id or job_id
  }
}

검증한 뒤 핸들러는 무엇을 해야 하나요?

이벤트를 기록하고, 응답하고, 느린 작업은 나중에 하세요. 전달 규약 전체는 Sume 영상 실행용 서명된 웹훅에서 다루며, 컨트롤러의 모양을 정하는 규칙은 다음과 같습니다.

  • 시도마다 10초가 주어지고 Sume는 최대 10회 시도하므로, 렌더링이나 다운로드는 204를 보내기 전에 하지 말고 큐에 넘기세요.
  • Run 웹훅은 재시도마다 같은 값이 반복되는 request_id로, 생성 Job 웹훅은 job_id로 중복을 제거하세요.
  • event로 분기하고, 처리하지 않는 이벤트 타입에는 500이 아니라 204로 응답하세요. 그래야 새 이벤트 타입이 재시도 폭주를 일으키지 않습니다.
  • 1 MiB를 넘는 영수증은 payload: null과 error.result_url을 담아 도착합니다. 그 URL에서 API 키로 영수증을 가져오세요.

출처

관련 글

연동 카테고리의 다른 글

연동 글 전체 보기

작성자 Sume