PPDF123
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,
   і заголовками полів; одна операція      промахом кешу та
   й конвеєр ділять цей ліміт              відповіддю 2xx від вищої операції
   понад: 400 + bad_request                понад: 500, у кеш нічого не пишеться
   (detail: читання multipart-поля не вдалося)

Підпис до рисунка: один ліміт, але на вхідному й вихідному боці він дає різні коди стану та протилежні наслідки.

Ліміт рахується по всьому тілу запиту, разом із межами

100 MiB обмежує тіло одного запиту, а не розмір окремого файлу й не розмір після розпакування.

  • Одна операція й багатокроковий конвеєр ділять це значення. Розбивши роботу на 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 — це справжній запис у таблиці кодів помилок. Він не провалюється в catch-all, а поділяє один код із помилкою в назві поля чи зламаним 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