Next 前端代理截断大文件请求体的排查记录



Next 前端代理截断大文件请求体的排查记录


日期:2026-03-24 场景:/posts/new 发布技能帖时,上传 MCP ZIP 到 POST /api/v1/skills/market/package/inspect


1. 问题现象


线上用户在发布技能帖时,已经选择了 ZIP 包,页面本地也能正常读取 mcp.json 并自动回填字段,但点击提交后服务端校验阶段失败。

实际出现过三类报错:

  • 502 backend_unreachable / fetch failed
  • 422 请求参数校验失败,body -> file: Field required
  • 500 Internal Server Error

这三类报错看起来像三个问题,实际上是同一条链路上不同阶段暴露出来的现象。


2. 请求链路


这条上传链路不是浏览器直接打后端,而是四段:

  1. 浏览器发起 POST /api/v1/skills/market/package/inspect
  2. 请求先进入 Next App Router 的 route handler
  3. Next 再把请求代理到后端 FastAPI
  4. 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 是否正确


接着重点检查浏览器到底发了什么。

在线上页面里做了两层拦截:

  1. XMLHttpRequest.send
  2. 拦 Playwright 的 network route

最终确认:

  • 请求体类型是 FormData
  • 其中确实有一个字段名为 file
  • 文件名是 china-travel-mcp.zip
  • 文件大小是 11259870
  • 浏览器请求头里的 Content-Type 已经是标准的 multipart/form-data; boundary=...

这一步的结论很关键:

  • 浏览器没有漏传文件
  • 字段名也没有拼错
  • boundary 也存在

因此,422 body -> file required 不是浏览器层的问题。


5. 第三阶段判断:为什么会先出现 422


虽然浏览器层是对的,但服务端一度返回过:

1
2
3
4
5
6
7
8
9
10
{
"code": 422,
"message": "请求参数校验失败",
"errors": [
{
"field": "body -> file",
"message": "Field required"
}
]
}

这个错误说明:

  • 请求已经到达后端
  • 但 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-length
  • content-encoding

并补了失败日志,方便区分是:

  • Next 连不上后端
  • 还是后端自己返回了 5xx

文件:

  • frontend/src/lib/server/proxy.ts

这些修复解决了最初的 502 fetch failed,也让请求更接近正确,但还没有解决最终问题。


6. 第四阶段判断:为什么后来变成 500


用户随后反馈线上变成了:

  • 500 Internal Server Error

这时直接去线上机器看 pod 日志,而不是继续猜。

44 机器上查看前端 pod 日志,命中了两条关键日志:

1
2
Request body exceeded 10MB for /api/v1/skills/market/package/inspect.
Only the first 10MB will be available unless configured.

以及:

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
2
3
experimental: {
proxyClientMaxBodySize: "32mb",
}

这样技能 ZIP 在常见场景下不会再被 Next 默认的 10MB 限制截断。


8.2 代理层对 multipart 重新建包再转发


frontend/src/lib/server/proxy.ts 中,针对 multipart/form-data

  1. request.formData()
  2. 再构造新的 FormData
  3. 把字段和文件重新 append
  4. 删除原始 content-type
  5. 让 Node 重新生成新的 boundary

这样可以避免二次转发时 header 和 body 不一致。

同时补充日志字段:

  • request_content_type
  • request_content_length
  • proxied_body_bytes
  • multipart_fields

这样后续如果还出问题,可以直接从日志看出代理层到底收到了哪些字段、文件名和大小。


9. 前端交互上的补充


因为这次问题和包大小直接相关,前端页面也补了两点:

  1. 上传区提示最大支持 32 MB
  2. 选中文件时先做前置大小校验,超限立即报错,不等提交

文件:

  • frontend/src/app/posts/new/page.tsx


10. 这次排查的有效经验


这次最有价值的不是某一行修复代码,而是排查顺序。

建议以后遇到类似用户明明传了文件,但后端说没收到的问题,按这个顺序查:

  1. 先看页面本地预检是否成功
  2. 再确认浏览器真实发出的 FormDataContent-Type
  3. 再看中间代理层有没有重写 header 或截断 body
  4. 最后再看后端是否真的没收到字段

不要一开始就把问题定性成:

  • 后端解析错了
  • 用户文件不合法
  • 线上网络不通

对于有前端代理的 Web 项目,中间层本身就是问题源,尤其是:

  • 大文件上传
  • multipart 二次转发
  • body limit
  • 默认代理缓冲策略


11. 最终结论


本次问题的最终根因是:

Next 前端代理对请求体的默认 10MB 上限,导致 10.996MB 的 ZIP 在进入 route handler 前被截断,后续 multipart 解析失败。

正确修复不是只改后端,也不是只改浏览器发包,而是同时做到:

  • 浏览器端不手写 multipart Content-Type
  • Next 代理层安全转发 multipart
  • Next 配置提升请求体上限
  • 页面上明确提示最大包大小

只有这几层一起符合,整条上传链路才稳定。