使用技巧2026-08-27约 1 分钟阅读

大 PDF 上传被拒:100 MiB 请求体上限的边界,与那个会误导排查方向的报错

单次请求的请求体上限是 100 MiB:正好 104,857,600 字节能过,多 1 字节就被拒,且按整个请求体算,含 multipart 的边界字节。超限时操作端点返回 400 与 code 为 bad_request 的错误,正文说读 multipart 字段失败,不是 413。带 Idempotency-Key 时另有一个 100 MiB 的响应缓冲上限,超限返回 500 且不写缓存。

PDF123 · 更新于 2026-08-27

大 PDF 传不上去,多数时候不是文件坏了,而是请求体撞上了单次请求的上限:100 MiB,也就是 104,857,600 字节。它按整个请求体算,含 multipart 的边界与字段头部,正好这么多能过,多 1 字节就被拒。

撞上之后的报错指向的是另一个方向。操作端点返回 400,code 是 bad_request,正文说读 multipart 字段失败,没有一处提到体积。按 code 分支的客户端会把它归进参数错误,然后去核对表单字段名,而真正要改的是文件大小。

同一个 100 MiB 还管着反方向。带 Idempotency-Key 的请求在缓存响应前要把响应体读进内存,那里设的是同一个数字;超限时调用方拿到 500,而操作其实已经执行完了。两件事共用一个数字,失效方式相反。

                    100 MiB = 104,857,600 字节(正好这么多能过)
                                 |
              +------------------+------------------+
              |                                     |
        入站(请求体)                          回传(响应体)
   按整个请求体算,含 multipart 边界与        仅在带 Idempotency-Key、
   字段头部;单次操作与 pipeline 共用        缓存未命中、上游 2xx 时缓冲
   超限:400 + bad_request                    超限:500,且不写缓存
   (正文说读 multipart 字段失败)

图注:同一个上限在入站与回传两侧,给出的是不同的状态码与相反的后果。

上限按整个请求体算,含边界字节

100 MiB 限制的是一次请求的请求体,不是单个文件的大小,也不是解压后的体积。

  • 单次操作与多步 pipeline 共用这个数。把任务拆成 10 步塞进一次 /api/v1/pipeline,上限不会因此变成 1 GB;步数只影响耗时。
  • 边界值本身可以通过:104,857,600 字节的请求体放行,104,857,601 字节不行。
  • 请求体里除了文件字节,还有每个字段的头部与边界分隔符,所以单个文件能用的额度严格小于 100 MiB。一个正好 104,857,600 字节的文件会被拒。

最后一条是实操里最容易踩空的地方:curl -F 会替你补上边界,拿文件大小去对那条线必然对不上。

超限时收到的是 400 与 bad_request

超限的响应不走 413,也不走任何与体积有关的措辞。用比上限多 1 字节的请求体复现,你会拿到这个:

head -c 104857601 /dev/zero > /tmp/over.bin
curl -s -X POST "$API_BASE/api/v1/misc/compress-pdf" \
  -H "X-API-KEY: $API_KEY" \
  -F "fileInput=@/tmp/over.bin"
HTTP/1.1 400 Bad Request
content-type: application/problem+json

{ "code": "bad_request",
  "detail": "failed to read multipart field: Error parsing `multipart/form-data` request",
  "hint": "Fix request parameters or upload a valid PDF.",
  "status": 400,
  "title": "Bad Request",
  "type": "https://pdf123.xyz/developers/errors#bad_request" }

三点要对着看:

  • 状态码是 400。读取请求体失败在本站被归成 bad_request,照常走 problem+json,所以按“超限即 413”写分支的客户端会走错路。
  • code 是 bad_request,错误码表里真实存在的一项。它不会落进兜底分支,而是和字段名写错、multipart 编码不对共用同一个码;那张表里没有任何一项与体积有关。
  • hint 建议你上传有效的 PDF。你上传的很可能就是一份有效 PDF,只是大了几百字节。

判断依据因此不在状态码,而在请求体字节数:拿到 400 且 detail 里出现 failed to read multipart field,下一步该量体积,不必回头检查表单。

413 在本站也确实会出现,只是不出现在操作端点上。能收文件上传的入口都被提到 100 MiB,不走上传的接口仍按 HTTP 框架(Rust 的 axum)默认的 2 MiB 处理,超限时返回 413 和一行纯文本:

head -c 2097153 /dev/zero > /tmp/big.json
curl -s -w '\n%{http_code}\n' -X POST "$API_BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  --data-binary @/tmp/big.json
Failed to buffer the request body: length limit exceeded
413

同一个事实,在两类端点上给出两种状态码、两种响应体。跨端点套用任何一条经验都会出错。

回传方向:同一个数字的另一种失效

带 Idempotency-Key 的请求会缓存响应,好在重试时直接回放。缓存要先把响应体读进内存,那里设了同样的 100 MiB,但触发条件窄得多,三件事同时成立才会走到:

  1. 请求带了 Idempotency-Key
  2. 缓存未命中,这个键是第一次见
  3. 上游操作返回的是 2xx

失败响应不缓存,直接放行,所以只有成功的大输出去撞这个上限。

撞上之后的行为是本文最该记住的一点:返回 500,并且什么都不写进缓存。 操作已经执行完,调用方却拿到失败;因为没缓存,重试会把整件事再跑一遍。幂等键本来是用来消除重复执行的,恰好在最需要它的时候失效,还把一个已完成的工作包装成服务端故障。这份缓存在进程内存里,不落盘,进程重启即失效;语义细节见 Idempotency-Key:PDF 作业的安全重试。

100 MiB 不是可配置的平台参数

这个值不能改。没有环境变量能把它调大或调小,两个方向都不能;想换成别的数字,只能改代码、重新构建镜像。把它当部署参数去找,会找不到地方。

它也不只是一条配额。请求体的字节会被完整读进内存之后再处理,每个并发的大上传都让服务端多占一份与之相当的内存。把上限往上调,同时要接受峰值内存随之上移:这个数字某种程度上也在替服务端挡住单个请求。

默认的自托管部署里没有反向代理,门户在提交前也不做体积校验,所以那一次拒绝来自服务端自己的上限。如果你自己在前端加了 nginx,先撞上的会是它:client_max_body_size 默认只放行 1 MiB,超过就返回 413,和上面那条对照里的形状接近,很容易被当成同一个限制。

撞上了怎么办

按代价从低到高:

  1. 先量后传。 发送前把请求体字节数与 104,857,600 比一次,并留出边界余量。这比事后读状态码可靠。
  2. 把文件压到上限以内。 扫描件的体积大多来自图像层,重新压缩常常能砍掉可观的一块。压缩 PDF 在浏览器里就能跑完,不用写脚本。
  3. 拆成多次调用。 内容本身可分的,拆分后分几次传,比调大上限省事,也不会把单次请求的内存占用推高。
  4. 确有一次传完的硬需求,才考虑改上限。 那属于改代码重建,并接受上一节那份内存代价。

这个上限要求客户端自己预判

两处失效指向同一个结论:100 MiB 需要客户端在发送前自己算。入站方向用 bad_request 回复你,那个码与体积无关;回传方向用 500 回复你,看起来像服务端故障。把字节数算在请求发出之前,是唯一不依赖报错语义的判断方式。

打开工具
在浏览器里完成处理,不加水印,文件处理完就会删除。
打开工具