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.

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/pipelinenie 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_requesti nadal przechodzi przez problem+json, więc klient napisany w założeniu, że przekroczenie limitu to 413, wybierze złą gałąź. codetobad_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.hintkaż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:
- Żądanie niosło
Idempotency-Key - W pamięci podręcznej nie było trafienia, więc klucz jest nowy
- 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:
- 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.
- 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.
- 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.
- 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.