Next 前端代理截断大文件请求体的排查记录
Next 前端代理截断大文件请求体的排查记录
日期:2026-03-24 场景:
/posts/new发布技能帖时,上传 MCP ZIP 到POST /api/v1/skills/market/package/inspect
1. 问题现象
线上用户在发布技能帖时,已经选择了 ZIP 包,页面本地也能正常读取
mcp.json
并自动回填字段,但点击提交后服务端校验阶段失败。
实际出现过三类报错:
502 backend_unreachable / fetch failed422 请求参数校验失败,body -> file: Field required500 Internal Server Error
这三类报错看起来像三个问题,实际上是同一条链路上不同阶段暴露出来的现象。
2. 请求链路
这条上传链路不是浏览器直接打后端,而是四段:
- 浏览器发起
POST /api/v1/skills/market/package/inspect - 请求先进入 Next App Router 的 route handler
- Next 再把请求代理到后端 FastAPI
- FastAPI 在
skills.py里读取UploadFile,解析 ZIP 和mcp.json
对应代码:
- 前端页面发请求:
frontend/src/app/posts/new/page.tsx - 前端上传调用:
frontend/src/lib/upload.ts - axios 实例:
frontend/src/lib/api.ts - Next
代理入口:
frontend/src/app/api/v1/[...path]/route.ts - Next 代理实现:
frontend/src/lib/server/proxy.ts - 后端校验入口:
backend/app/routers/v1/skills.py
3. 第一阶段判断:不是 ZIP 本身不合法
最先看到的现象是:
- 选择 ZIP 后,页面已经自动填出了技能名、显示名、版本号、入口文件
这说明至少两件事成立:
- 浏览器侧确实拿到了 ZIP 文件
- 本地预检逻辑已经成功读到了
mcp.json
所以问题不在用户没选文件,也不在ZIP 格式一定错误,而在服务端复检链路。
4. 第二阶段判断:浏览器发出的 multipart 是否正确
接着重点检查浏览器到底发了什么。
在线上页面里做了两层拦截:
- 拦
XMLHttpRequest.send - 拦 Playwright 的 network route
最终确认:
- 请求体类型是
FormData - 其中确实有一个字段名为
file - 文件名是
china-travel-mcp.zip - 文件大小是
11259870 - 浏览器请求头里的
Content-Type已经是标准的multipart/form-data; boundary=...
这一步的结论很关键:
- 浏览器没有漏传文件
- 字段名也没有拼错
- boundary 也存在
因此,422 body -> file required
不是浏览器层的问题。
5. 第三阶段判断:为什么会先出现 422
虽然浏览器层是对的,但服务端一度返回过:
1 | { |
这个错误说明:
- 请求已经到达后端
- 但 FastAPI 没把它识别成一个合法的
multipart文件字段
这时重点怀疑的是代理层二次转发把 multipart 搞坏了。
于是做了两类修复:
5.1 浏览器侧不再手写
Content-Type
在这些地方移除了手写的 multipart 头:
frontend/src/lib/upload.ts
同时在 axios 请求拦截器里加了保护:
- 如果
config.data instanceof FormData - 就删除默认的
Content-Type - 让浏览器自己生成 boundary
文件:
frontend/src/lib/api.ts
5.2 Next 代理不再透传有风险的长度/编码头
在代理层删除了:
content-lengthcontent-encoding
并补了失败日志,方便区分是:
- Next 连不上后端
- 还是后端自己返回了 5xx
文件:
frontend/src/lib/server/proxy.ts
这些修复解决了最初的
502 fetch failed,也让请求更接近正确,但还没有解决最终问题。
6. 第四阶段判断:为什么后来变成 500
用户随后反馈线上变成了:
500 Internal Server Error
这时直接去线上机器看 pod 日志,而不是继续猜。
在 44 机器上查看前端 pod 日志,命中了两条关键日志:
1 | Request body exceeded 10MB for /api/v1/skills/market/package/inspect. |
以及:
1 | TypeError: Failed to parse body as FormData. |
这两条日志直接把根因钉死了。
7. 根因
根因不是后端,也不是浏览器,而是 Next 前端在启用
src/proxy.ts 的情况下,对请求体有默认 10MB
上限。
而这次上传的 ZIP 实际大小是:
11259870字节- 大约
10.996 MB
也就是说:
- 浏览器把完整文件发到了 Next
- Next 在进入 route handler 前先缓冲请求体
- 因为默认只允许 10MB,所以它把请求体截断了
- 随后
request.formData()读取的是一个被截断的 multipart body - 最终就会出现:
- 有时解析失败直接 500
- 有时后端看到的表单不完整,表现成 422
file required
这也是为什么:
- 普通接口都正常
- 只有这个大文件上传接口出问题
因为问题不是前端完全连不上后端,而是Next 在处理大 multipart body 时先把包截断了。
8. 修复方案
修复分两部分。
8.1 提高 Next 的请求体上限
在 frontend/next.config.ts 中加入:
1 | experimental: { |
这样技能 ZIP 在常见场景下不会再被 Next 默认的 10MB 限制截断。
8.2 代理层对 multipart 重新建包再转发
在 frontend/src/lib/server/proxy.ts 中,针对
multipart/form-data:
- 先
request.formData() - 再构造新的
FormData - 把字段和文件重新 append
- 删除原始
content-type - 让 Node 重新生成新的 boundary
这样可以避免二次转发时 header 和 body 不一致。
同时补充日志字段:
request_content_typerequest_content_lengthproxied_body_bytesmultipart_fields
这样后续如果还出问题,可以直接从日志看出代理层到底收到了哪些字段、文件名和大小。
9. 前端交互上的补充
因为这次问题和包大小直接相关,前端页面也补了两点:
- 上传区提示最大支持
32 MB - 选中文件时先做前置大小校验,超限立即报错,不等提交
文件:
frontend/src/app/posts/new/page.tsx
10. 这次排查的有效经验
这次最有价值的不是某一行修复代码,而是排查顺序。
建议以后遇到类似用户明明传了文件,但后端说没收到的问题,按这个顺序查:
- 先看页面本地预检是否成功
- 再确认浏览器真实发出的
FormData和Content-Type - 再看中间代理层有没有重写 header 或截断 body
- 最后再看后端是否真的没收到字段
不要一开始就把问题定性成:
- 后端解析错了
- 用户文件不合法
- 线上网络不通
对于有前端代理的 Web 项目,中间层本身就是问题源,尤其是:
- 大文件上传
- multipart 二次转发
- body limit
- 默认代理缓冲策略
11. 最终结论
本次问题的最终根因是:
Next 前端代理对请求体的默认 10MB 上限,导致 10.996MB 的 ZIP 在进入 route handler 前被截断,后续 multipart 解析失败。
正确修复不是只改后端,也不是只改浏览器发包,而是同时做到:
- 浏览器端不手写 multipart
Content-Type - Next 代理层安全转发 multipart
- Next 配置提升请求体上限
- 页面上明确提示最大包大小
只有这几层一起符合,整条上传链路才稳定。