How-to2026-08-27約 1 分鐘閱讀

大型 PDF 上傳被拒:100 MiB 請求本體上限,與那個指向錯誤方向的訊息

單次請求本體上限是 100 MiB,也就是 104,857,600 位元組,含 multipart 框標;超過時操作端點回傳 400 與 bad_request,而不是 413,內容只說 multipart 欄位讀取失敗,帶 Idempotency-Key 時回應緩衝另有 100 MiB 上限,超過回傳 500 且不寫入快取。

PDF123 · Updated 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、
   邊界與欄位標頭;單次操作與             快取未命中且上游 2xx 時
   pipeline 共用
   超過: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 的話,先撞上的會是 nginx:client_max_body_size 預設只允許 1 MiB,超過就回 413,形狀和上面那個對照很接近,很容易被誤認成同一個限制。

撞上了該怎麼辦

從成本最低的開始:

  1. 送出前先量。 在請求送出前把請求本體的位元組數和 104,857,600 比一次,並留出邊界的餘裕。這比事後讀狀態碼可靠。
  2. 把檔案壓到上限以內。 掃描檔的體積大多來自影像層,重新壓縮通常能明顯減少一部分。壓縮 PDF 在瀏覽器裡就能跑,不需要寫指令碼。
  3. 把工作拆成多次呼叫。 內容本身可分割時,拆分後分幾次送出:比提高上限省事,也不會推高單次請求所佔的記憶體。
  4. 只有確有單次請求的硬需求,才去改上限。 那意味著改程式碼、重新建置,並接受上一節說的記憶體代價。

這個上限要求用戶端事先自行判斷

兩處失效指向同一個結論:100 MiB 這個數字必須由用戶端在送出前自己算出來。入站方向用 bad_request 回你,那個碼完全沒說體積;回傳方向用 500 回你,看起來像伺服器故障。在請求離開前把位元組數算清楚,是唯一不依賴錯誤訊息措辭的判斷方式。

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool