How-to2026-08-276 мин четене

Когато голям PDF бъде отхвърлен: лимитът от 100 MiB за тялото на заявката и грешката, която подвежда

Тялото на една заявка може да е 100 MiB, тоест 104,857,600 байта заедно с рамкирането на multipart; над лимита крайните точки на операциите отговарят с 400 и код bad_request, с пояснение за неуспешно multipart поле, а не с 413, затова проблемът с размера изглежда като проблем с параметър; с Idempotency-Key важи втори лимит от 100 MiB за буфера на отговора и при превишаването му получавате 500, без нищо записано в кеша.

PDF123 · Updated 2026-08-27

Голям PDF, който не се качва, обикновено не е повреден файл. Ударил е лимитът за тялото на един запрос: 100 MiB, тоест 104,857,600 байта. Той се брои по цялото тяло, включително границите на multipart и заглавките на полетата: точно толкова минава, а с един байт повече — не.

Грешката, която се връща, сочи на друго място. Крайните точки на операциите отговарят с 400, code е bad_request, а в полето detail се казва, че multipart поле не е прочетено; за размер не се споменава никъде. Клиент, който се разклонява по code, записва това като грешка в параметрите и отива да проверява имената на полетата, докато промяната трябва да е в размера на файла.

Същите 100 MiB управляват и обратната посока. Заявка с Idempotency-Key прочита отговора в паметта, преди да го кешира, и там важи същото число; при превишаване извикващият получава 500, макар операцията вече да е приключила. Едно число, два противоположни режима на отказ.

                     100 MiB = 104,857,600 байта (точно толкова минава)
                                   |
                +------------------+------------------+
                |                                     |
         входящ (тяло на заявката)             изходящ (тяло на отговора)
   цялото тяло, границите на multipart     само с Idempotency-Key,
   и заглавките на полетата; една          пропуск в кеша и
   операция и pipeline споделят лимита     отговор 2xx от горната операция
   над лимита: 400 + bad_request           над лимита: 500, нищо не се кешира
   (detail: четене на 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 и изобщо не съдържа дума за размер. Възпроизведете го с тяло с един байт повече:

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, а крайните точки без качване продължават да работят с подразбиращите се 2 MiB на HTTP рамката (в Rust — axum); тяло над лимита получава 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, което изглежда като сървърен отказ. Броенето на байтовете, преди заявката да тръгне, е единственият начин да прецените, без да зависи от текста на съобщението за грешка.

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