tRPC 配合 Express 5,上传报 Failed to parse body as FormData
Express 5 前置 parser 可能留下 undefined body 属性,tRPC适配层误把未读multipart流当已处理。隔离tRPC与REST解析顺序,保留认证、大小限制和其他接口。
tRPC 的 JSON procedure 正常,FormData 输入却统一报 Failed to parse body as FormData。如果 Express 5 在 tRPC 之前注册了 express.json() 或 express.urlencoded(),检查请求对象是否已经有一个值为 undefined 的 body 属性,而原始 multipart 流其实还没被读取。
在 tRPC 11.18.0 的这个组合里,问题不是前面的 JSON parser 把文件解析坏了。body-parser 2.x 对不匹配的类型跳过解析,但仍可留下 req.body = undefined;tRPC 的 Node 适配层看到 'body' in req,将它视为已经处理过正文,构造了没有 body 的 Fetch Request。后面的 request.formData() 才报错。
字段存在,不等于正文已经消费
先核对实际挂载顺序、Content-Type、req.body 的值和流结束状态。只记录必要的状态,不打印上传内容或认证头。
如果 multipart 本来应交给 tRPC,前置 parser 不匹配类型时,原始流仍可能可读;但“body 属性存在”的判断已经把它截断在适配阶段。反过来,某个上传中间件已经真正消费了流,也不能靠删除 body 属性来恢复原始字节。
因此不要在所有请求上统一 delete req.body,不要复制一个空对象来假装已经解析成功,也不要给 FormData 请求换上 application/json 头。它们会改变其他请求的契约,且没有解决正文所有权。
将 tRPC 与 REST 的正文解析边界分开
对于应用自己维护的 Express 装配,可以让 tRPC 适配层先处理它的专用路径,REST parser 仍用于后面的 REST 路由。下面只示意顺序,router 和 restRouter 沿用项目原有定义:
app.use('/trpc', createExpressMiddleware({ router }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/rest', restRouter);
认证、请求日志、限流和其他必须覆盖 tRPC 的控制应继续位于合适的前置位置,不能随着 parser 一起挪到 tRPC 后面。也不要删除全站 JSON parser,让 REST 接口在修复上传后失去请求正文。
tRPC 自己仍要保留正文类型处理、大小限制与 procedure 输入校验。客户端发送 FormData 时,由对应实现生成与正文一致的 multipart boundary;手写一个没有 boundary 的 Content-Type,是另一条格式错误路径。
NestJS 等框架可能替应用注册 parser,不能照搬这几行就认定顺序已经变化。应使用框架支持的注册与路径隔离方式,核对真正到达 tRPC adapter 的请求,避免私改内部请求属性。
正式版本仍需核对 undefined 分支
截至 2026 年 10 月 23 日核对的 tRPC 11.19.0 正式适配源码,仍把存在但为 undefined 的 body 直接转换为无正文。不能把某个修正分支已经存在,理解成当前安装包已经按可读状态回退到流式处理。
若改用后续兼容版本,应核对它如何区分“parser 跳过”与“流已消费”,而不是只用一个全新 Express 应用验证没有前置 parser 的情况。
验收要沿实际中间件链发送原来的 FormData,确认 procedure 收到完整字段和文件;同时覆盖原有 tRPC JSON 调用、REST JSON 与 URL 编码请求,以及格式错误和超出大小限制的失败路径。上传会产生业务记录时,先核对此前是否已经被接收,再决定重试,不能仅凭客户端解析错误无条件重新提交。


