How-to2026-08-276 min czytania

Gdy duży PDF zostaje odrzucony: limit treści żądania 100 MiB i błąd, który kieruje w złą stronę

Treść jednego żądania może mieć 100 MiB, czyli 104,857,600 bajtów wraz z obramowaniem multipart; po przekroczeniu limitu punkty końcowe operacji odpowiadają 400 z kodem bad_request i informacją o nieudanym polu multipart, a nie 413, więc problem rozmiaru wygląda jak problem parametru; z Idempotency-Key obowiązuje drugi limit 100 MiB na bufor odpowiedzi, a po jego przekroczeniu dostajesz 500 i nic nie trafia do pamięci podręcznej.

PDF123 · Updated 2026-08-27

Duży plik PDF, który nie chce się przesłać, zwykle nie jest uszkodzony. Zadziałał limit treści pojedynczego żądania: 100 MiB, czyli 104,857,600 bajtów. Liczy się on przez całą treść, razem z granicami multipart i nagłówkami pól: dokładnie tyle przechodzi, a o jeden bajt więcej zostaje odrzucone.

Błąd, który wraca, wskazuje gdzie indziej. Punkty końcowe operacji odpowiadają 400, code ma wartość bad_request, a w polu detail jest mowa o nieudanym odczycie pola multipart; o rozmiarze nie ma ani słowa. Klient, który rozgałęzia się po code, zaklasyfikuje to jako błąd parametru i pójdzie sprawdzać nazwy pól, choć zmienić trzeba rozmiar pliku.

Ten sam 100 MiB rządzi też kierunkiem odwrotnym. Żądanie z Idempotency-Key wczytuje odpowiedź do pamięci przed zapisaniem jej w cache i tam obowiązuje ta sama wartość; po jej przekroczeniu wywołujący dostaje 500, choć operacja już się zakończyła. Jedna liczba, dwa przeciwne tryby awarii.

                     100 MiB = 104,857,600 bajtów (dokładnie tyle przechodzi)
                                   |
                +------------------+------------------+
                |                                     |
         przychodzące (treść żądania)          wychodzące (treść odpowiedzi)
   cała treść, granice multipart           tylko z Idempotency-Key,
   i nagłówki pól; jedna operacja          brakiem trafienia w cache i
   i potok dzielą ten limit                odpowiedzią 2xx z nadrzędnej operacji
   powyżej: 400 + bad_request              powyżej: 500, nic nie trafia do cache
   (detail: odczyt pola multipart się nie powiódł)

Podpis rysunku: jeden limit, a po stronie przychodzącej i wychodzącej daje różne kody stanu i przeciwne skutki.

Limit obejmuje całą treść żądania, razem z granicami

Wartość 100 MiB ogranicza treść pojedynczego żądania, a nie rozmiar jednego pliku ani rozmiar po dekompresji.

  • Jedna operacja i wieloetapowy potok dzielą tę wartość. Rozbicie pracy na 10 kroków w jednym wywołaniu /api/v1/pipeline nie zamieni pułapu na 1 GB; liczba kroków wpływa tylko na czas wykonania.
  • Sama wartość graniczna przechodzi: treść żądania o rozmiarze 104,857,600 bajtów przejdzie, a 104,857,601 bajtów już nie.
  • Treść zawiera nagłówki każdego pola i separatory granic, a nie tylko bajty pliku, więc na pojedynczy plik zostaje ściśle mniej niż 100 MiB. Plik o rozmiarze dokładnie 104,857,600 bajtów zostanie odrzucony.

Właśnie na tym ostatnim punkcie praktyka potyka się najczęściej: curl -F dodaje granice za ciebie, więc porównywanie rozmiaru pliku z tą wartością nigdy się nie zgodzi.

Po przekroczeniu limitu dostajesz 400 i bad_request

Odpowiedź po przekroczeniu limitu nie używa 413 i nie zawiera żadnego słowa o rozmiarze. Odtwórz ją, wysyłając treść o jeden bajt większą:

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" }

Trzy rzeczy, które trzeba odczytać razem:

  • Kod stanu to 400. Nieudany odczyt treści jest tu klasyfikowany jako bad_request i nadal przechodzi przez problem+json, więc klient napisany w założeniu, że przekroczenie limitu to 413, wybierze złą gałąź.
  • code to bad_request, prawdziwy wpis w tabeli kodów błędów. Nie wpada do gałęzi domyślnej, tylko dzieli jeden kod z literówką w nazwie pola albo uszkodzonym kodowaniem multipart, a nic w tej tabeli nie dotyczy rozmiaru.
  • hint każe przesłać poprawny plik PDF. Plik, który wysłałeś, najprawdopodobniej jest poprawnym plikiem PDF, tylko o kilkaset bajtów za dużym.

Tropem nie jest więc kod stanu, lecz liczba bajtów treści: przy 400, którego detail zawiera failed to read multipart field, zmierz najpierw rozmiar, zamiast przechodzić ponownie przez formularz.

Kod 413 też się tu pojawia, tylko nie w punktach końcowych operacji. Wszystkie punkty wejścia przyjmujące plik zostały podniesione do 100 MiB, a punkty końcowe bez przesyłania plików nadal działają na domyślnych 2 MiB frameworka HTTP (w Ruście axum); treść ponad limit dostaje 413 i jedną linijkę zwykłego tekstu:

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

Jeden fakt, dwa kody stanu i dwie różne treści odpowiedzi w zależności od punktu końcowego. Przenoszenie doświadczenia z jednego na drugi wprowadzi cię w błąd.

Druga awaria z tą samą liczbą, w drodze powrotnej

Żądanie z Idempotency-Key zapisuje swoją odpowiedź w pamięci podręcznej, aby ponowienie mogło ją odtworzyć. Zapis w cache oznacza najpierw wczytanie treści odpowiedzi do pamięci, a ten odczyt ogranicza to samo 100 MiB — tyle że wymaga znacznie węższego zestawu warunków:

  1. Żądanie niosło Idempotency-Key
  2. W pamięci podręcznej nie było trafienia, więc klucz jest nowy
  3. Operacja nadrzędna zwróciła 2xx

Awarie nie są zapisywane w cache i przechodzą dalej, więc do tego limitu dociera tylko udany duży wynik.

Właśnie tam dzieje się rzecz warta zapamiętania: dostajesz 500, a do pamięci podręcznej nie trafia nic. Operacja już się wykonała, a wywołujący widzi błąd; ponieważ nic nie zapisano w cache, ponowienie uruchamia całość od nowa. Klucz idempotencji istnieje po to, by eliminować powtórzoną pracę, a zawodzi dokładnie wtedy, gdy jest najbardziej potrzebny, przedstawiając ukończone zadanie jako awarię serwera. Pamięć podręczna żyje w pamięci procesu i nigdy nie trafia na dysk, więc restart ją czyści; o semantyce przeczytasz w Idempotency-Key: bezpieczne ponawianie zadań PDF.

100 MiB to nie konfigurowalne ustawienie platformy

Tej wartości nie da się zmienić. Żadna zmienna środowiskowa jej nie podnosi ani nie obniża, w żadną stronę; inna liczba oznacza zmianę kodu i przebudowę obrazu. Szukaj jej w ustawieniach wdrożenia — nie znajdziesz.

To także coś więcej niż kwota. Bajty treści żądania są wczytywane do pamięci w całości przed przetworzeniem, więc każde równoległe duże przesłanie trzyma obok porównywalną ilość. Podniesienie pułapu oznacza zgodę na wyższy szczyt zużycia pamięci: ta liczba jest też tym, co nie pozwala pojedynczemu żądaniu pociągnąć procesu w dół.

Domyślne wdrożenie samodzielnie hostowane nie ma odwrotnego proxy, a portal nie sprawdza rozmiaru przed wysłaniem, więc odrzucenie pochodzi z własnego limitu serwera. Postaw z przodu nginx i najpierw trafisz na niego: client_max_body_size domyślnie przepuszcza tylko 1 MiB i odpowiada 413 — kształt bliski temu z powyższego porównania, łatwy do pomylenia z tym samym limitem.

Co zrobić, gdy na niego trafisz

Najpierw najtańsze:

  1. Najpierw zmierz, potem wyślij. Porównaj liczbę bajtów treści żądania z 104,857,600 przed wysłaniem i zostaw zapas na granice. To pewniejsze niż czytanie kodu stanu po fakcie.
  2. Skompresuj plik poniżej limitu. Rozmiar skanu bierze się głównie z warstwy obrazu, a ponowna kompresja zwykle zdejmuje z niej widoczną część. Kompresuj PDF działa w przeglądarce, skrypt nie jest potrzebny.
  3. Podziel pracę na kilka wywołań. Gdy treść da się podzielić, podziel ją i wyślij w kilku żądaniach: mniej kłopotu niż podnoszenie pułapu i nie zwiększa pamięci zajmowanej przez jedno żądanie.
  4. Zmieniaj limit tylko przy twardym wymogu jednego żądania. To oznacza zmianę kodu, przebudowę i zgodę na koszt pamięci z poprzedniej sekcji.

Ten limit wymaga od klienta decyzji z wyprzedzeniem

Oba błędy prowadzą do jednego wniosku: wartość 100 MiB klient musi ustalić sam, przed wysłaniem. Na wejściu odpowiada bad_request — kod, który nic nie mówi o rozmiarze; na wyjściu 500, co wygląda jak awaria serwera. Liczenie bajtów, zanim żądanie wyruszy, to jedyny sposób oceny, który nie zależy od treści komunikatu błędu.

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