Limites de taille d’envoi d’images en PHP
Un « Upload failed » pour une grande photo peut venir de l’un de cinq endroits, et en PHP deux d’entre eux se trouvent dans php.ini et agissent avant l’exécution de votre script. Cette page liste chaque couche dans l’ordre où une requête la rencontre, ce que chacune renvoie, et un comportement de PHP qui transforme un 413 propre en page HTML si vous ne modifiez pas un réglage.
Les couches, dans l’ordre
- La boîte de dialogue de la galerie. Avec
maxUploadFileSize: 4000000dans les options de l’éditeur, la boîte de dialogue refuse un fichier plus grand avant de l’envoyer. Un fichier de 4.5 Mo a donné « big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB. » (la boîte de dialogue compte en mégaoctets binaires). C’est une commodité. Le serveur ne peut pas s’y fier. - Le serveur web. La documentation de nginx donne à
client_max_body_sizeune valeur par défaut de 1m : une photo de 2 Mo est donc refusée avant que PHP ne la voie, avec la page HTML 413 propre à nginx. L’équivalent sous Apache estLimitRequestBody. Le fichiernginx.conf.examplede l’exemple définitclient_max_body_size 5m;. post_max_size(8M par défaut) limite l’ensemble du corps de la requête.upload_max_filesize(2M par défaut) limite chaque fichier.- Votre propre vérification : 4,000,000 octets dans l’exemple, avec une réponse 413 en JSON.
Pour que la limite de l’exemple soit celle qui s’applique, chaque couche précédente doit autoriser un peu plus de 4,000,000 octets. L’encodage multipart ajoute quelques centaines d’octets autour du fichier.
upload_max_filesize = 5M
post_max_size = 8M
display_errors = Off
log_errors = OnComment le script les distingue
// 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.');
}Au-delà de upload_max_filesize, PHP exécute quand même le script, et $_FILES['file']['error'] vaut UPLOAD_ERR_INI_SIZE. Nous avons envoyé un fichier de 3 Mo à un serveur utilisant la valeur par défaut de PHP, 2M, et obtenu 413 {"error":"The image is larger than the server accepts."}.
Au-delà de post_max_size, PHP jette tout le corps de la requête : $_POST et $_FILES sont tous deux des tableaux vides, comme si rien n’avait été envoyé. Le script s’exécute quand même, et $_SERVER['CONTENT_LENGTH'] contient toujours la taille réelle. Cette combinaison est le seul moyen de savoir ce qui s’est passé : l’exemple la vérifie donc avant toute autre chose. Si vous omettez cette vérification, un envoi qui dépasse la limite ressemble à un « aucun fichier reçu ».
Le piège de display_errors
Quand une requête dépasse post_max_size, PHP émet aussi un avertissement, et il le fait avant la première ligne de votre script. Avec display_errors activé, ce qui est la valeur par défaut intégrée de PHP lorsqu’aucun php.ini n’est chargé, et aussi le réglage de php.ini-development, cet avertissement devient le début du corps de la réponse. Votre http_response_code(413) échoue alors avec « Cannot modify header information », et le navigateur reçoit ceci :
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 ne peut pas réparer cela, car la sortie a été envoyée avant son démarrage. Désactivez l’affichage des erreurs et journalisez-les plutôt. Avec le serveur intégré de PHP, -d display_errors=stderr n’a pas aidé lors de notre test. Seul -d display_errors=0 a donné un HTTP/1.1 413 propre avec Content-Type: application/json, et l’avertissement est alors parti dans le journal du serveur. php.ini-production contient déjà display_errors = Off.
Le dossier temporaire et open_basedir
PHP écrit chaque fichier envoyé dans un fichier temporaire avant l’exécution de votre script, dans upload_tmp_dir ou dans le dossier temporaire du système. Si vous utilisez open_basedir sans y inclure ce dossier, tous les envois échouent, quelle que soit leur taille. Le journal indique « open_basedir restriction in effect » et « unable to create a temporary file », et le script voit UPLOAD_ERR_NO_TMP_DIR, auquel l’exemple répond par un 500. Après l’ajout du dossier temporaire à open_basedir, les 35 vérifications du test de contrat sont passées. Sous Windows, -d open_basedir=A;B en ligne de commande ne conserve discrètement que A, car ; commence un commentaire ini. Mettez la valeur entre guillemets : -d 'open_basedir="A;B"'.
Tester
Démarrez le projet d’exemple de ce guide avec php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php, puis envoyez un fichier qui dépasse la limite de l’exemple et un autre qui dépasse 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)."}Les deux réponses sont des HTTP 413 avec un corps JSON. Pour observer le piège, redémarrez le serveur sans -d display_errors=0 et envoyez de nouveau huge.jpg. Avec un PHP sans php.ini, ou avec php.ini-development, l’avertissement HTML arrive devant le JSON.
Le même guide pour d’autres plateformes
JavaScript · Next.js · ASP.NET Core · MVC 5 · Web Forms