Quand un gros PDF ne monte pas : la limite de 100 MiB par requête et l'erreur qui vous oriente mal
Le corps d'une requête peut atteindre 100 MiB, soit 104,857,600 octets, cadre multipart compris. Au-delà de la limite, les points de terminaison d'opération répondent 400 avec le code bad_request et un détail sur un champ multipart illisible, pas 413 : un problème de taille se lit donc comme un problème de paramètres. Avec Idempotency-Key, une seconde limite de 100 MiB s'applique au tampon de réponse ; au-delà, vous recevez 500 et rien n'est mis en cache.

Un PDF volumineux qui ne monte pas n'est en général pas un fichier corrompu. Le corps de la requête a heurté la limite par requête : 100 MiB, soit 104,857,600 octets. Elle est mesurée sur le corps entier, délimiteurs multipart et en-têtes de champ compris, et ce chiffre exact passe tandis qu'un octet de plus est rejeté.
L'erreur qui revient pointe ailleurs. Les points de terminaison d'opération répondent 400 avec code à bad_request et un détail indiquant qu'un champ multipart n'a pas pu être lu, sans aucune mention de la taille. Un client qui branche sur code classe cela comme une erreur de paramètre et part vérifier les noms de champs, alors que ce qu'il faut changer, c'est la taille du fichier.
Les mêmes 100 MiB gouvernent aussi le sens inverse. Une requête portant Idempotency-Key lit la réponse en mémoire avant de la mettre en cache, contre ce même chiffre, et au-delà l'appelant reçoit 500 alors que l'opération est déjà terminée. Un seul chiffre, deux modes de défaillance opposés.
100 MiB = 104,857,600 octets (ce chiffre exact passe)
|
+------------------+------------------+
| |
entrant (corps de la requête) sortant (corps de la réponse)
corps entier, délimiteurs multipart et uniquement avec Idempotency-Key,
en-têtes de champ inclus ; une opération et un défaut de cache et un
un pipeline le partagent 2xx amont
dépassé : 400 + bad_request dépassé : 500, rien en cache
(détail : un champ multipart a échoué)
Note de figure : une seule limite, et ses côtés entrant et sortant renvoient des codes de statut différents aux conséquences opposées.
La limite porte sur le corps entier de la requête, délimiteurs compris
Les 100 MiB plafonnent le corps d'une seule requête, pas la taille d'un fichier ni la taille après décompression.
- Une opération et un pipeline à plusieurs étapes partagent ce chiffre. Découper le travail en 10 étapes dans un appel à
/api/v1/pipelinene transforme pas le plafond en 1 GB ; le nombre d'étapes n'affecte que le temps d'exécution. - La valeur limite exacte passe : un corps de 104,857,600 octets est accepté, 104,857,601 octets ne le sont pas.
- Le corps porte les en-têtes de chaque champ et les délimiteurs en plus des octets du fichier, donc la marge restante pour un seul fichier est strictement inférieure à 100 MiB. Un fichier de exactement 104,857,600 octets est rejeté.
C'est sur ce dernier point que la pratique dérape le plus facilement : curl -F ajoute les délimiteurs à votre place, donc comparer la taille d'un fichier à cette ligne ne peut jamais tomber juste.
Au-delà de la limite, vous recevez 400 et bad_request
Une réponse au-delà de la limite n'utilise pas 413 et ne comporte aucune mention de la taille. Reproduisez-la avec un corps un octet trop grand :
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" }
Trois éléments sont à lire ensemble :
- Le statut est 400. Un échec de lecture du corps est classé ici comme
bad_requestet passe malgré tout par problem+json, donc un client conçu pour « au-delà de la limite, c'est 413 » prend la mauvaise branche. codevautbad_request, une entrée réelle de la table des codes d'erreur. Il ne tombe pas dans un fourre-tout ; il partage un même code avec un nom de champ mal orthographié ou un encodage multipart cassé, et rien dans cette table ne concerne la taille.- Le
hintvous invite à téléverser un PDF valide. Le fichier que vous avez envoyé est très probablement un PDF valide, simplement plus gros de quelques centaines d'octets.
L'indice n'est donc pas le code de statut mais le nombre d'octets du corps : sur un 400 dont le detail contient failed to read multipart field, mesurez la taille au lieu de repasser le formulaire.
413 existe bien sur ce site, mais pas sur les points de terminaison d'opération. Chaque point d'entrée qui accepte un téléversement a été relevé à 100 MiB, tandis que les points de terminaison sans téléversement restent au défaut du framework HTTP (axum, en Rust), 2 MiB, où un corps au-delà de la limite reçoit 413 et une ligne de texte brut :
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
Un même fait, deux codes de statut et deux corps de réponse selon le point de terminaison. Transposer l'une des deux expériences à l'autre vous induira en erreur.
L'autre défaillance portant le même chiffre, au retour
Une requête avec Idempotency-Key met sa réponse en cache pour qu'une nouvelle tentative puisse la rejouer. Mettre en cache suppose de lire d'abord le corps de la réponse en mémoire, et cette lecture est plafonnée par les mêmes 100 MiB, même si les conditions sont bien plus étroites :
- La requête portait
Idempotency-Key - Le cache a manqué, donc cette clé est nouvelle
- L'opération amont a renvoyé 2xx
Les échecs ne sont pas mis en cache et passent directement, donc seule une sortie volumineuse et réussie atteint cette limite.
Ce qui s'y produit mérite d'être retenu : vous recevez 500, et rien n'est écrit dans le cache. L'opération a déjà tourné, mais l'appelant voit un échec ; comme rien n'a été mis en cache, la nouvelle tentative refait tout le travail. La clé d'idempotence existe pour éliminer le travail en double et défaille précisément quand on en a le plus besoin, en déguisant une tâche terminée en panne serveur. Le cache vit dans la mémoire du processus et n'est jamais écrit sur disque, donc un redémarrage le vide ; pour la sémantique, voir Idempotency-Key : des reprises sûres pour les tâches PDF.
100 MiB n'est pas un réglage configurable de la plateforme
Le chiffre ne peut pas être modifié. Aucune variable d'environnement ne l'augmente ni ne le diminue, dans aucun sens ; un autre nombre suppose de changer le code et de reconstruire l'image. Cherchez-le comme réglage de déploiement : vous ne le trouverez pas.
C'est aussi plus qu'un quota. Les octets d'un corps de requête sont lus intégralement en mémoire avant traitement, donc chaque téléversement volumineux simultané en occupe une quantité comparable à côté. Relever le plafond revient à accepter un pic de mémoire plus haut : le chiffre est aussi ce qui empêche une seule requête de faire plier le processus.
Un déploiement auto-hébergé par défaut n'a pas de proxy inverse, et le portail ne vérifie pas la taille avant l'envoi, donc ce refus vient de la limite propre au serveur. Placez nginx devant et vous le heurterez d'abord : client_max_body_size n'autorise que 1 MiB par défaut et répond 413, une forme proche de celle de la comparaison ci-dessus et facile à confondre avec la même limite.
Que faire lorsque vous le heurtez
Du moins coûteux au plus coûteux :
- Mesurez avant d'envoyer. Comparez le nombre d'octets du corps à 104,857,600 avant que la requête parte, en gardant de la marge pour les délimiteurs. C'est plus fiable que de lire un code de statut après coup.
- Compressez le fichier sous la limite. La taille d'un scan vient surtout de sa couche d'image, et recompresser en retire régulièrement une part visible. Compresser un PDF tourne dans le navigateur, sans script.
- Répartissez le travail sur plusieurs appels. Quand le contenu se divise, découpez-le et envoyez-le en quelques requêtes : moins d'ennuis que de relever le plafond, et cela n'augmente pas la mémoire qu'une requête retient.
- Ne changez la limite que pour un besoin impératif de requête unique. Cela suppose de modifier le code, de reconstruire, et d'accepter le coût mémoire de la section précédente.
Cette limite oblige le client à décider à l'avance
Les deux défaillances mènent à une même conclusion : le client doit calculer le chiffre de 100 MiB avant d'envoyer. À l'entrée, on vous répond bad_request, un code qui ne dit rien sur la taille ; à la sortie, 500, qui ressemble à une panne serveur. Compter les octets avant le départ de la requête est le seul jugement qui ne dépende pas de ce que dit un message d'erreur.