Wenn ein großes PDF beim Hochladen abgelehnt wird: das 100-MiB-Limit für den Anfragekörper und der Fehler, der in die falsche Richtung führt
Ein Anfragekörper darf 100 MiB groß sein, also 104.857.600 Bytes inklusive Multipart-Rahmen; über dem Limit antworten Endpunkte für Operationen mit 400 und dem Code bad_request sowie einem Detail zu einem fehlgeschlagenen Multipart-Feld, nicht mit 413, und mit Idempotency-Key gilt ein zweites Limit von 100 MiB für den Antwortpuffer, bei dessen Überschreitung 500 kommt und nichts zwischengespeichert wird.

Ein großes PDF, das sich nicht hochladen lässt, ist meist keine beschädigte Datei. Der Anfragekörper hat das Limit pro Anfrage erreicht: 100 MiB bzw. 104.857.600 Bytes. Gemessen wird über den ganzen Körper, Multipart-Grenzen und Feld-Header eingeschlossen; genau dieser Wert geht durch, ein einziges Byte mehr wird abgelehnt.
Der Fehler, der zurückkommt, weist in eine andere Richtung. Endpunkte für Operationen antworten mit 400, code steht auf bad_request, und das Detail meldet, dass ein Multipart-Feld nicht gelesen werden konnte; von der Größe ist nirgends die Rede. Ein Client, der anhand von code verzweigt, legt das als Parameterfehler ab und macht sich daran, Feldnamen zu prüfen, obwohl die Dateigröße das ist, was geändert werden muss.
Dieselben 100 MiB gelten auch in der Gegenrichtung. Eine Anfrage mit Idempotency-Key liest die Antwort vor dem Zwischenspeichern in den Speicher ein, gegen denselben Wert, und darüber erhält der Aufrufer 500, während die Operation bereits abgeschlossen ist. Eine Zahl, zwei entgegengesetzte Fehlerbilder.
100 MiB = 104.857.600 Bytes (genau dieser Wert geht durch)
|
+------------------+------------------+
| |
eingehend (Anfragekörper) ausgehend (Antwortkörper)
ganzer Körper, Multipart-Grenzen und nur mit Idempotency-Key,
Feld-Header eingeschlossen; eine ein Cache-Miss und ein
Operation und eine Pipeline teilen ihn Upstream-2xx
darüber: 400 + bad_request darüber: 500, kein Cache
(Detail: Multipart-Feld nicht lesbar)
Hinweis zur Abbildung: ein Limit, und die eingehende und die ausgehende Seite liefern unterschiedliche Statuscodes mit entgegengesetzten Folgen.
Das Limit zählt den ganzen Anfragekörper, Grenzen eingeschlossen
Die 100 MiB begrenzen den Körper einer einzelnen Anfrage, nicht die Größe einer einzelnen Datei und nicht die Größe nach der Dekompression.
- Eine einzelne Operation und eine mehrstufige Pipeline teilen sich diesen Wert. Die Arbeit in 10 Schritte innerhalb eines Aufrufs von
/api/v1/pipelineaufzuteilen macht aus der Obergrenze keine 1 GB; die Schrittzahl wirkt sich nur auf die Laufzeit aus. - Der Grenzwert selbst geht durch: Ein Anfragekörper mit 104.857.600 Bytes wird akzeptiert, 104.857.601 Bytes nicht.
- Der Körper enthält neben den Dateibytes auch die Header jedes Felds und die Begrenzungszeichen, der Spielraum für eine einzelne Datei liegt also strikt unter 100 MiB. Eine Datei mit genau 104.857.600 Bytes wird abgelehnt.
Genau an diesem letzten Punkt geht die Praxis am leichtesten daneben: curl -F fügt die Begrenzungen selbst hinzu, ein Vergleich der Dateigröße mit dieser Marke kann also nie aufgehen.
Über dem Limit bekommst du 400 und bad_request
Eine Antwort über dem Limit nutzt kein 413 und enthält überhaupt keinen Wortlaut zur Größe. Reproduzieren lässt sich das mit einem Körper, der ein Byte darüber liegt:
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" }
Drei Dinge sind zusammen zu lesen:
- Der Status ist 400. Ein fehlgeschlagenes Lesen des Körpers wird hier als
bad_requesteingeordnet und läuft weiterhin über problem+json; ein Client, der für „über dem Limit heißt 413“ geschrieben ist, nimmt den falschen Zweig. codeistbad_request, ein echter Eintrag in der Fehlercodetabelle. Er fällt nicht in einen Sammelzweig, sondern teilt sich einen Code mit einem falsch geschriebenen Feldnamen oder einer kaputten Multipart-Kodierung, und nichts in dieser Tabelle betrifft die Größe.- Der
hinträt dir, ein gültiges PDF hochzuladen. Die Datei, die du hochgeladen hast, ist sehr wahrscheinlich ein gültiges PDF, das zufällig ein paar hundert Bytes zu groß ist.
Das Erkennungsmerkmal ist also nicht der Statuscode, sondern die Byteanzahl des Körpers: Bei einer 400, deren detail den Text failed to read multipart field enthält, miss als Nächstes die Größe, statt das Formular noch einmal durchzugehen.
413 kommt auf dieser Website durchaus vor, nur nicht an Endpunkten für Operationen. Jeder Einstiegspunkt, der einen Dateiupload annimmt, wurde auf 100 MiB angehoben, während Endpunkte ohne Upload weiterhin mit der Standardgrenze des HTTP-Frameworks (axum in Rust) von 2 MiB laufen; dort bekommt ein zu großer Körper 413 und eine Zeile Klartext:
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
Ein Sachverhalt, zwei Statuscodes und zwei Antwortkörper je nach Endpunkt. Wer eine der beiden Erfahrungen auf die andere überträgt, wird in die Irre geführt.
Der andere Fehler mit derselben Zahl, auf dem Rückweg
Eine Anfrage mit Idempotency-Key speichert ihre Antwort zwischen, damit ein erneuter Versuch sie wiedergeben kann. Zwischenspeichern heißt, den Antwortkörper zuerst in den Speicher zu lesen, und dieses Einlesen ist auf dieselben 100 MiB begrenzt, allerdings greift es nur unter deutlich engeren Bedingungen:
- Die Anfrage trug
Idempotency-Key. - Der Cache wurde nicht getroffen, der Schlüssel ist also neu.
- Die vorgelagerte Operation hat 2xx zurückgegeben.
Fehlschläge werden nicht zwischengespeichert und laufen direkt durch; nur eine erfolgreiche große Ausgabe erreicht dieses Limit.
Was dort passiert, ist der Punkt, den man sich merken sollte: Du bekommst 500, und in den Cache wird nichts geschrieben. Die Operation ist bereits gelaufen, der Aufrufer sieht trotzdem einen Fehlschlag; weil nichts zwischengespeichert wurde, führt der erneute Versuch alles noch einmal aus. Der Idempotency-Key soll doppelte Arbeit vermeiden und versagt genau dann, wenn er am dringendsten gebraucht wird, indem er einen abgeschlossenen Auftrag als Serverfehler ausgibt. Der Cache liegt im Prozessspeicher und wird nie auf die Festplatte geschrieben, ein Neustart löscht ihn also; zur Semantik siehe Idempotency-Key: sichere Wiederholungen für PDF-Aufgaben.
100 MiB ist keine konfigurierbare Plattform-Einstellung
Der Wert lässt sich nicht ändern. Keine Umgebungsvariable hebt oder senkt ihn, in keine Richtung; eine andere Zahl bedeutet, den Code zu ändern und das Image neu zu bauen. Suchst du ihn als Deployment-Einstellung, wirst du keine finden.
Er ist auch mehr als eine Quote. Die Bytes eines Anfragekörpers werden vor der Verarbeitung vollständig in den Speicher gelesen, jeder gleichzeitige große Upload belegt daneben also eine vergleichbare Menge. Die Obergrenze anzuheben heißt, auch einen höheren Speicherbedarf in der Spitze zu akzeptieren: Diese Zahl ist zugleich das, was verhindert, dass eine einzelne Anfrage den Prozess mit nach unten zieht.
Eine standardmäßige selbst gehostete Installation hat keinen Reverse-Proxy, und das Portal prüft die Größe vor dem Absenden nicht, die Ablehnung stammt also vom eigenen Limit des Servers. Setzt du nginx davor, triffst du zuerst auf nginx: client_max_body_size erlaubt standardmäßig nur 1 MiB und antwortet mit 413, eine Form, die der aus dem obigen Vergleich nahekommt und leicht für dasselbe Limit gehalten wird.
Was tun, wenn du es triffst
Zuerst das Billigste:
- Erst messen, dann senden. Vergleiche die Byteanzahl des Anfragekörpers vor dem Absenden mit 104.857.600 und lass Platz für die Begrenzungen. Das ist besser, als hinterher einen Statuscode zu lesen.
- Die Datei unter das Limit komprimieren. Die Größe eines Scans stammt überwiegend aus seiner Bildebene, und erneutes Komprimieren entfernt regelmäßig einen sichtbaren Anteil davon. PDF komprimieren läuft im Browser, ohne Skript.
- Die Arbeit auf mehrere Aufrufe verteilen. Wenn sich der Inhalt aufteilen lässt, teile ihn auf und sende ihn in einigen Anfragen: weniger Aufwand, als die Obergrenze anzuheben, und es treibt den Speicher, den eine Anfrage hält, nicht nach oben.
- Ändere das Limit nur bei einem harten Erfordernis, alles in einer Anfrage zu senden. Das bedeutet, Code zu ändern und neu zu bauen, und den Speicherpreis aus dem vorigen Abschnitt zu akzeptieren.
Dieses Limit verlangt, dass der Client vorab entscheidet
Beide Fehlerbilder führen zu einer Schlussfolgerung: Die 100 MiB muss der Client vor dem Senden selbst berechnen. Eingehend bekommst du bad_request als Antwort, einen Code, der nichts über die Größe aussagt; ausgehend 500, was wie ein Serverfehler aussieht. Die Bytes zu zählen, bevor die Anfrage rausgeht, ist die einzige Beurteilung, die nicht davon abhängt, was eine Fehlermeldung sagt.