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

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 동작 | Sume 웹훅의 경우 |
|---|---|---|
NestFactory.create()의 rawBody: true | 파싱하지 않은 본문을 보관. 내장 body parser가 필요 | 필수. 서명은 원본 바이트를 대상으로 함 |
RawBodyRequest<Request> | req.rawBody를 Buffer로 노출 | Buffer를 그대로 verifyWebhook에 전달 |
app.useBodyParser("json", { limit }) | Express 기본값: 100kb | 1 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/sdk0.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 키로 영수증을 가져오세요.
출처
관련 글
연동 카테고리의 다른 글
- Open WebUI MCP 서버: Sume 호스팅 MCP 연결하기
Open WebUI에서는 관리자가 Sume 호스팅 MCP를 추가합니다. Type은 MCP (Streamable HTTP)로 두고, 인증은 사용자별 OAuth 2.1이나 공유 Bearer 키 하나를 씁니다.
- OpenAI Agents SDK MCP 서버: Sume와 5초 타임아웃
API 키로 OpenAI Agents SDK를 Sume 호스팅 MCP 서버에 연결하고, 기본 5초인 클라이언트 타임아웃을 jobs_wait의 55초보다 길게 늘리세요.
- OpenAI Responses API MCP 도구로 Sume 호출
Sume 호스팅 MCP 서버를 Responses API에 mcp 도구로 추가하고, Sume API 키는 headers로 보내고, 유료 도구 호출은 실행 전에 승인하세요.
- OpenClaw MCP 서버: Sume 이미지·영상 도구 추가하기
openclaw mcp add로 Sume 호스팅 MCP 서버를 OpenClaw에 저장하고, OAuth나 API 키 헤더로 로그인한 뒤, requestTimeoutMs를 55,000보다 크게 설정하세요.
작성자 Sume