# 手动 app.use(express.json()) 会让 NestJS `rawBody: true` 静默失效

> **日期**: 2026-05-14
> **类型**: NestJS / Express 中间件顺序陷阱
> **影响**: 所有 webhook 签名校验（Gitea / Stripe / Slack / 钉钉）会持续返 invalid_signature

## 现象

internal-app-platform 上测试服，webhook HMAC 校验一直失败：

```bash
node -e '
const body = JSON.stringify({...});
const sig = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
// POST 到 webhook 端点带这个 sig
' 
# → 永远返 invalid_signature
```

后端代码：
```ts
const rawBody = req.rawBody?.toString('utf8') ?? '';
const expected = createHmac('sha256', this.secret).update(rawBody).digest('hex');
```

`req.rawBody` 永远是 `undefined`，因此 expected HMAC 永远是空字符串的 hash，永远不等于客户端发的 sig。

## 根因

`NestFactory.create(AppModule, { bodyParser: true, rawBody: true })` 期望由 **NestJS 自己的 body parser** 解析请求体（解析时同步存 `req.rawBody`）。但 `main.ts` 启动后又显式：

```ts
app.use(require('express').json({ limit: '50mb' }));
```

Express 中间件**先注册先执行**——这个手动注册的 `express.json` 比 NestJS 的 bodyParser 先跑，**先把 body 流读光并解析成 JSON**。等 NestJS bodyParser 跑时，请求 body 流已空，`rawBody` 没东西可存。

这跟 Express 的内部机制有关：`json()` 中间件读流是一次性消费，下游 middleware 拿到的是已解析的 `req.body`，但**没有 `req.rawBody`**。

## 解决

给手动 `express.json` 加 `verify` 回调，自己存 `rawBody`：

```ts
app.use(require('express').json({
  limit: '50mb',
  verify: (req: any, _res: unknown, buf: Buffer) => {
    req.rawBody = buf;
  },
}));
```

`verify` 是 Express body-parser 的标准 hook，每次解析前用原始 Buffer 调用，可在此存任何 metadata。

## 类比 / 其他可选

| 方案 | 适用 | 缺点 |
|---|---|---|
| `verify` 回调存 rawBody (本次采纳) | 已有手动 json() 的项目 | 需要业务方都用 `req.rawBody` 这个字段名 |
| 删掉手动 `express.json`，纯靠 NestJS rawBody:true | 重构窗口 | 需改 limit 配置传递路径 |
| 用 `Buffer` body + 单独 webhook 路由用 raw-body 中间件 | 完全隔离 | 配置散落，可读性差 |
| webhook 自带 `@RawBody()` 装饰器（NestJS 内置） | 单点需求 | 仍需要 NestJS bodyParser 接管该路由 |

## 触发条件 / 判别

下次怀疑此问题：
1. 后端日志显示 webhook 走到 controller 但 HMAC 永远失败
2. **客户端 + 服务端用同一份 crypto lib 计算同样的 body 仍然不匹配** → 极强信号是 rawBody 没拿到
3. 直接打印 `req.rawBody`：如果是 `undefined` → 命中本陷阱
4. 检查 main.ts 是否有手动 `app.use(express.json())`

## 适用面

- 任何 NestJS + Express 项目接 webhook
- 任何手动叠加 body parser 中间件的场景
- 接 Stripe / Slack / Gitea / GitHub / 钉钉 / 飞书 webhook 都涉及 HMAC over raw body
- **不仅是 NestJS**：Express 任何想保留 rawBody 的项目都要走 `verify` callback 这条路

## 状态

- ✅ main.ts 加 verify 回调
- ✅ webhook HMAC 校验真路径打通
- 预期效果：Stripe/Slack 等其他 webhook 改造也免疫
