Cuando un PDF grande no sube: el límite de 100 MiB y el error que apunta en la dirección equivocada
El cuerpo de una petición puede llegar a 100 MiB, es decir 104,857,600 bytes, con el marco multipart incluido. Por encima del límite, los endpoints de operación responden 400 con el código bad_request y un detalle sobre un campo multipart que no se pudo leer, no 413, así que un problema de tamaño se lee como un problema de parámetros. Con Idempotency-Key hay un segundo límite de 100 MiB en el búfer de respuesta; por encima de él recibes 500 y no se cachea nada.

Un PDF grande que no sube normalmente no está corrupto. El cuerpo de la petición chocó con el límite por petición: 100 MiB, o 104,857,600 bytes. Se mide sobre el cuerpo entero, incluidos los delimitadores multipart y las cabeceras de cada campo, y esa cifra exacta pasa mientras que un byte más se rechaza.
El error que devuelve apunta a otro sitio. Los endpoints de operación responden 400 con code igual a bad_request y un detalle que dice que no se pudo leer un campo multipart, sin mencionar el tamaño en ningún momento. Un cliente que ramifica por code lo archiva como error de parámetros y se va a comprobar los nombres de los campos, cuando lo que hay que cambiar es el tamaño del archivo.
El mismo 100 MiB también gobierna la dirección contraria. Una petición con Idempotency-Key lee la respuesta en memoria antes de cachearla, contra la misma cifra, y por encima de ella el llamador recibe 500 mientras la operación ya ha terminado. Un solo número, dos modos de fallo opuestos.
100 MiB = 104,857,600 bytes (esta cifra exacta pasa)
|
+------------------+------------------+
| |
entrante (cuerpo de la petición) saliente (cuerpo de la respuesta)
cuerpo entero, delimitadores multipart y solo con Idempotency-Key,
cabeceras de campo incluidos; una operación un fallo de caché y un
y una pipeline lo comparten 2xx del origen
superado: 400 + bad_request superado: 500, nada en caché
(detalle: falló un campo multipart)
Nota de la figura: un solo límite, y sus lados entrante y saliente devuelven códigos de estado distintos con consecuencias opuestas.
El límite cuenta el cuerpo entero de la petición, delimitadores incluidos
Los 100 MiB limitan el cuerpo de una sola petición, no el tamaño de un archivo ni el tamaño después de descomprimir.
- Una operación y una pipeline de varios pasos comparten la cifra. Dividir el trabajo en 10 pasos dentro de una llamada a
/api/v1/pipelineno convierte el techo en 1 GB; el número de pasos solo afecta al tiempo de ejecución. - El valor límite exacto sí pasa: un cuerpo de 104,857,600 bytes entra, 104,857,601 bytes no.
- El cuerpo lleva las cabeceras de cada campo y los delimitadores además de los bytes del archivo, así que el margen que le queda a un solo archivo es estrictamente inferior a 100 MiB. Un archivo de exactamente 104,857,600 bytes se rechaza.
Ese último punto es donde la práctica se pierde con más facilidad: curl -F añade los delimitadores por ti, así que comparar el tamaño de un archivo con esa línea nunca puede salir bien.
Al superar el límite recibes 400 y bad_request
Una respuesta por encima del límite no usa 413 y no lleva ninguna mención al tamaño. Repródúcela con un cuerpo un byte por encima:
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" }
Conviene leer tres cosas juntas:
- El estado es 400. Un fallo al leer el cuerpo se clasifica aquí como
bad_requesty aun así pasa por problem+json, así que un cliente escrito para "por encima del límite significa 413" toma la rama equivocada. codeesbad_request, una entrada real en la tabla de códigos de error. No cae en un comodín; comparte un mismo código con un nombre de campo mal escrito o una codificación multipart rota, y nada en esa tabla tiene que ver con el tamaño.- El
hintte dice que subas un PDF válido. El archivo que subiste es muy probablemente un PDF válido que resulta ser unos cientos de bytes demasiado grande.
La pista, entonces, no está en el código de estado sino en el número de bytes del cuerpo: ante un 400 cuyo detail contenga failed to read multipart field, mide el tamaño en vez de repasar el formulario.
413 sí aparece en este sitio, solo que no en los endpoints de operación. Todos los puntos de entrada que aceptan una subida de archivo se han elevado a 100 MiB, mientras que los endpoints que no reciben subida siguen con el valor por defecto del framework HTTP (axum, de Rust), 2 MiB, donde un cuerpo por encima del límite recibe 413 y una línea de texto plano:
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 mismo hecho, dos códigos de estado y dos cuerpos de respuesta según el endpoint. Trasladar cualquiera de las dos experiencias a la otra te llevará a engaño.
El otro fallo con el mismo número, a la vuelta
Una petición con Idempotency-Key cachea su respuesta para que un reintento pueda reproducirla. Cachear significa leer antes el cuerpo de la respuesta en memoria, y esa lectura está limitada por los mismos 100 MiB, aunque hacen falta muchas menos condiciones:
- La petición llevaba
Idempotency-Key - La caché falló, así que la clave era nueva
- La operación de origen devolvió 2xx
Los fallos no se cachean y pasan directamente, así que solo una salida grande y correcta llega a este límite.
Lo que ocurre ahí es lo que merece recordarse: recibes 500 y no se escribe nada en la caché. La operación ya se ha ejecutado, pero el llamador ve un fallo; como no se cacheó nada, el reintento vuelve a hacer todo el trabajo. La clave de idempotencia existe para eliminar trabajo duplicado y falla justo cuando más se necesita, disfrazando un trabajo terminado de avería del servidor. La caché vive en la memoria del proceso y nunca se escribe en disco, así que un reinicio la vacía; para la semántica, consulta Idempotency-Key: reintentos seguros para trabajos PDF.
100 MiB no es un parámetro configurable de la plataforma
La cifra no se puede cambiar. Ninguna variable de entorno la sube ni la baja, en ningún sentido; un número distinto significa tocar el código y reconstruir la imagen. Búscala como parámetro de despliegue y no la encontrarás.
También es más que una cuota. Los bytes de un cuerpo de petición se leen enteros en memoria antes de procesarlos, así que cada subida grande concurrente ocupa otra cantidad comparable al lado. Subir el techo implica aceptar junto con él un pico de memoria mayor: el número es además lo que impide que una sola petición arrastre el proceso.
Un despliegue autoalojado por defecto no tiene proxy inverso, y el portal no comprueba el tamaño antes de enviar, así que ese rechazo viene del propio límite del servidor. Si pones nginx delante, chocarás antes con nginx: client_max_body_size solo permite 1 MiB por defecto y responde 413, una forma parecida a la de la comparación anterior y fácil de confundir con el mismo límite.
Qué hacer cuando lo topas
De lo más barato a lo más caro:
- Mide antes de enviar. Compara el número de bytes del cuerpo con 104,857,600 antes de que salga la petición y deja margen para los delimitadores. Eso gana a leer un código de estado después.
- Comprime el archivo por debajo del límite. El tamaño de un escaneo viene sobre todo de su capa de imagen, y recomprimir suele quitarle una parte visible. Comprimir un PDF funciona en el navegador, sin scripts.
- Reparte el trabajo en varias llamadas. Cuando el contenido se puede dividir, divídelo y envíalo en unas pocas peticiones: menos problemas que subir el techo, y no aumenta la memoria que ocupa una petición.
- Cambia el límite solo si necesitas una única petición sí o sí. Eso implica tocar el código, reconstruir y aceptar el coste de memoria de la sección anterior.
Este límite obliga al cliente a decidir de antemano
Los dos fallos llevan a una misma conclusión: el cliente tiene que calcular la cifra de 100 MiB antes de enviar. De entrada te responde bad_request, un código que no dice nada sobre el tamaño; de salida, 500, que parece una avería del servidor. Contar los bytes antes de que la petición salga es la única forma de juzgar que no depende de lo que diga un mensaje de error.