Límites de tamaño de carga de imágenes en PHP
Un "Upload failed" con una foto grande puede venir de cualquiera de cinco sitios, y en PHP dos de ellos están en php.ini y actúan antes de que se ejecute tu script. Esta página enumera cada capa en el orden en que la encuentra una solicitud, qué devuelve cada una y un comportamiento de PHP que convierte un 413 limpio en una página HTML a menos que cambies un ajuste.
Las capas, en orden
- El cuadro de diálogo de la galería. Con
maxUploadFileSize: 4000000en las opciones del editor, el cuadro de diálogo rechaza un archivo más grande antes de enviarlo. Un archivo de 4.5 MB dio "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." (el cuadro de diálogo cuenta en megabytes binarios). Es una comodidad. El servidor no puede depender de ello. - El servidor web. La documentación de nginx indica que
client_max_body_sizevale 1m de forma predeterminada, así que una foto de 2 MB se rechaza antes de que PHP la vea, con la página HTML 413 propia de nginx. El equivalente en Apache esLimitRequestBody. Elnginx.conf.exampledel ejemplo establececlient_max_body_size 5m;. post_max_size(8M de forma predeterminada) limita el cuerpo completo de la solicitud.upload_max_filesize(2M de forma predeterminada) limita cada archivo.- Tu propia comprobación: 4,000,000 bytes en el ejemplo, con una respuesta 413 en JSON.
Para que el límite que se aplique sea el del ejemplo, cada capa anterior debe permitir algo más de 4,000,000 bytes. La codificación multipart añade unos cientos de bytes alrededor del archivo.
upload_max_filesize = 5M
post_max_size = 8M
display_errors = Off
log_errors = OnCómo las distingue el script
// Over post_max_size, PHP drops the WHOLE body: $_POST and $_FILES are both empty.
$length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
if (empty($_FILES) && empty($_POST) && $length > rte_ini_bytes('post_max_size')) {
throw new RteHttpError(413, 'The upload is larger than the server accepts (post_max_size).');
}
$file = $_FILES['file'] ?? null;
// One file only: "file[]" arrives as nested arrays, which this refuses.
if (!is_array($file) || !is_int($file['error'] ?? null) || !is_string($file['tmp_name'] ?? null)) {
throw new RteHttpError(400, 'No file received. Send it as the "file" field.');
}
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE: // larger than upload_max_filesize
case UPLOAD_ERR_FORM_SIZE: // larger than a MAX_FILE_SIZE form field
throw new RteHttpError(413, 'The image is larger than the server accepts.');
case UPLOAD_ERR_PARTIAL:
throw new RteHttpError(400, 'The upload was interrupted. Please try again.');
case UPLOAD_ERR_NO_FILE:
throw new RteHttpError(400, 'No file received.');
default: // NO_TMP_DIR, CANT_WRITE, EXTENSION: server-side problems
error_log('rte upload: UPLOAD_ERR code ' . $file['error']);
throw new RteHttpError(500, 'The server could not store the upload.');
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RteHttpError(400, 'No file received.');
}
$size = filesize($file['tmp_name']);
if ($size === false || $size === 0) {
throw new RteHttpError(400, 'The file is empty.');
}
if ($size > RTE_MAX_BYTES) {
throw new RteHttpError(413, 'Images can be at most 4,000,000 bytes.');
}Por encima de upload_max_filesize, PHP sigue ejecutando el script, y $_FILES['file']['error'] es UPLOAD_ERR_INI_SIZE. Enviamos un archivo de 3 MB a un servidor que usaba el valor predeterminado de PHP, 2M, y obtuvimos 413 {"error":"The image is larger than the server accepts."}.
Por encima de post_max_size, PHP descarta el cuerpo entero: $_POST y $_FILES son arrays vacíos, como si no se hubiera enviado nada. El script se sigue ejecutando, y $_SERVER['CONTENT_LENGTH'] sigue conteniendo el tamaño real. Esa combinación es la única forma de saber qué ocurrió, así que el ejemplo la comprueba antes que nada. Si te saltas esta comprobación, una carga que supera el límite parece un "no se recibió ningún archivo".
La trampa de display_errors
Cuando una solicitud supera post_max_size, PHP también emite una advertencia, y lo hace antes de la primera línea de tu script. Con display_errors activado, que es el valor predeterminado integrado de PHP cuando no se carga ningún php.ini y también el ajuste de php.ini-development, esa advertencia se convierte en el principio del cuerpo de la respuesta. Tu http_response_code(413) falla entonces con "Cannot modify header information", y el navegador recibe esto:
HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8
<br />
<b>Warning</b>: POST Content-Length of 9000204 bytes exceeds the limit of 8388608 bytes in <b>Unknown</b> on line <b>0</b><br />
...
{"error":"The upload is larger than the server accepts (post_max_size)."}Un script no puede arreglar esto, porque la salida se envió antes de que empezara el script. Desactiva la visualización de errores y regístralos en el log. Con el servidor integrado de PHP, -d display_errors=stderr no ayudó en nuestra prueba. Solo -d display_errors=0 dio un HTTP/1.1 413 limpio con Content-Type: application/json, y la advertencia pasó entonces al log del servidor. php.ini-production ya tiene display_errors = Off.
La carpeta temporal y open_basedir
PHP escribe cada carga en un archivo temporal antes de que se ejecute tu script, en upload_tmp_dir o en la carpeta temporal del sistema. Si usas open_basedir y dejas fuera esa carpeta, todas las cargas fallan, sea cual sea su tamaño. El log muestra "open_basedir restriction in effect" y "unable to create a temporary file", y el script ve UPLOAD_ERR_NO_TMP_DIR, al que el ejemplo responde con un 500. Después de añadir la carpeta temporal a open_basedir, pasaron las 35 comprobaciones del contrato. En Windows, -d open_basedir=A;B en la línea de comandos conserva sin avisar solo A, porque ; inicia un comentario de ini. Pon el valor entre comillas: -d 'open_basedir="A;B"'.
Pruébalo
Inicia el proyecto de ejemplo de esta guía con php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php y envía un archivo que supere el límite del ejemplo y otro que supere post_max_size:
head -c 4500000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5201/rte/upload
{"error":"Images can be at most 4,000,000 bytes."}
head -c 9000000 /dev/zero > huge.jpg
curl -F "file=@huge.jpg" http://localhost:5201/rte/upload
{"error":"The upload is larger than the server accepts (post_max_size)."}Ambas respuestas son HTTP 413 con un cuerpo JSON. Para ver la trampa, vuelve a iniciar el servidor sin -d display_errors=0 y envía huge.jpg otra vez. En un PHP sin php.ini, o con php.ini-development, la advertencia en HTML llega delante del JSON.
La misma guía para otras plataformas
JavaScript · Next.js · ASP.NET Core · MVC 5 · Web Forms