meta charset utf-8 : des 404 sur des adresses d'images coréennes cassées par le robot AdSense et un php-fpm saturé
Si le lecteur ne trouve pas la déclaration meta charset utf-8 au début du HTML et que l'en-tête de réponse n'a pas de charset non plus, il doit deviner le codage des caractères du document ou le lire avec une valeur par défaut. Sur un serveur WordPress en configuration Nginx et Apache, le robot d'exploration AdSense a envoyé une rafale de requêtes vers des adresses d'images coréennes (non ASCII) cassées, et le traitement des 404 a occupé tous les workers PHP. Après avoir suivi l'incident dans les logs, j'ai ajouté charset à l'en-tête de réponse des pages en cache et fait répondre nginx directement par un 404 pour les fichiers statiques absents, afin que les mêmes requêtes ne remplissent plus les workers.
1️⃣ meta charset utf-8 : pourquoi il doit se trouver dans les 1 024 premiers octets
Le serveur peut indiquer le codage des caractères du document à deux endroits : le charset ajouté au champ d'en-tête de réponse [Content-Type], et la ligne meta charset écrite dans le HTML.
Le standard HTML impose que la déclaration de codage figure uniquement dans les 1 024 premiers octets, et recommande au lecteur de ne lire à l'avance que ces 1 024 premiers octets pour la trouver. Ainsi, même dans la balise head, un meta charset repoussé après 1 024 octets peut échapper à certains lecteurs.
Pour choisir le codage, le lecteur regarde d'abord le BOM (byte order mark), puis le charset de l'en-tête de réponse, puis le meta charset trouvé par la lecture préalable des 1 024 premiers octets. Si aucun des trois n'est présent, il devine à partir de la forme des octets ou applique une valeur par défaut, et dans le tableau des valeurs par défaut proposé par le standard HTML, la plupart des locale ont windows-1252.
Lorsqu'un document UTF-8 est lu en windows-1252, un seul caractère sur plusieurs octets, comme une lettre coréenne, se transforme en plusieurs autres caractères.
2️⃣ Adresses d'images coréennes (non ASCII) cassées par le robot d'exploration AdSense Mediapartners-Google
En vérifiant la page en cache d'avant la correction au regard des 1 024 premiers octets, le champ d'en-tête de réponse Content-Type n'avait pas de charset et la ligne meta charset se trouvait à l'octet 15 199. La page en cache reçue par le robot d'exploration AdSense Mediapartners-Google n'offrait donc de codage trouvable à aucun des deux endroits.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlDans cette configuration, ajouter une chaîne de requête à l'adresse contourne la page en cache et renvoie une réponse produite directement par PHP, et cette réponse portait bien un charset.
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8La position à laquelle commence le meta charset dans le fichier de cache se vérifie à l'octet près avec [grep -b].
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charsetLes balises img du même fichier de cache contenaient des noms de fichiers multilingues en UTF-8 (ici en coréen) tels quels, sans encodage-pourcent. Si ce HTML est lu en windows-1252, un nom comme [대시보드-화면-예시.png] devient des caractères comme [대시보드-화면-예시.png], et si ces caractères sont de nouveau encodés-pourcent en UTF-8 puis demandés, le serveur cherche un fichier qui n'existe pas.
En extrayant du log d'accès les 404 d'images reçus par ce robot, les adresses aux noms cassés s'enchaînent.
grep 'Mediapartners-Google' /var/log/nginx/access.log | grep ' 404 ' | grep 'GET /wp-content/uploads/' | cut -d' ' -f1,4,7,9 | head -2203.0.113.3 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---1024x682.png 404
203.0.113.1 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---300x159.jpg 404Les adresses restées dans le log étaient identiques, octet pour octet, à la valeur obtenue en lisant les octets UTF-8 du nom de fichier d'origine comme des caractères windows-1252 puis en les encodant de nouveau en UTF-8. Ce jour-là, le log comptait 202 requêtes vers des adresses d'images cassées de cette façon venant de ce robot, 6 venant d'un script (python-requests), et 0 venant de visiteurs.
Si vous passez le nom cassé [대시보드-화면-예시.png] dans l'outil Convertisseur UTF-8, il est identifié comme des octets UTF-8 lus en windows-1252, et le nom d'origine [대시보드-화면-예시.png] réapparaît.
3️⃣ Comment les 404 d'images absentes ont rempli php-fpm
Comme les images des adresses cassées n'existent pas sur le serveur, nginx a transmis les requêtes à Apache selon le dernier élément de try_files de sa règle location pour les images, et WordPress a produit la page 404 avec PHP. Sur les 587 404 enregistrés en journée ce même jour, chaque page 404 sans cache a pris une médiane de 1,4 seconde.
Les requêtes d'images aux noms cassés se sont accumulées jusqu'à un peu plus de 200 en 3 minutes à partir de 20:40, et 133 d'entre elles se sont terminées en 404. Chaque requête reçoit un worker php-fpm, et comme les requêtes continuaient d'arriver pendant que chaque 404 prenait 4 à 6 secondes, le pool a atteint sa limite à 20:40:55.
Le moment où le pool atteint sa limite est consigné dans le journal d'erreurs de php-fpm comme avertissement php-fpm max_children.
grep 'max_children' /var/log/php-fpm/error.log[03-Nov-2026 20:40:55] WARNING: [pool www] server reached pm.max_children setting (20), consider raising itLes requêtes arrivées après le remplissage du pool ont attendu un worker libre. Le 404 qui a attendu le plus longtemps s'est terminé au bout de 58 secondes, les requêtes que le robot a coupées après 60 secondes d'attente ont été enregistrées en 499, et une requête restée sans réponse du backend en 504.
Le préchargement du cache, les tâches planifiées (Action Scheduler) qui tournaient à la même heure et wp-cron, lancé chaque minute, ont été vérifiés et écartés comme cause. Le préchargement tournait toute la journée au même rythme faible, les tâches planifiées ne traitaient que des travaux de préchargement à ce moment-là, et wp-cron s'exécute avec PHP en ligne de commande, donc sans workers php-fpm.
4️⃣ nginx charset utf-8 et 404 immédiat pour les fichiers statiques absents
J'ai ajouté la ligne [charset utf-8;] à la location nginx qui sert les pages en cache pour que l'en-tête de réponse porte un charset, et pour les images et fichiers statiques du dossier d'envoi, nginx renvoie désormais directement un 404 s'ils ne sont pas sur le disque, au lieu de les transmettre à WordPress.
# Bloc http : valeur Cache-Control définie uniquement pour les réponses 404
map $status $up404_cc { 404 'no-store'; default ''; }
# location qui sert directement le HTML en cache
location / {
try_files $cache_file @backend;
charset utf-8;
}
# Dossier d'envoi : les images absentes reçoivent un 404 direct sans passer par le backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}La map se place dans le bloc http, et comme nginx n'envoie pas un en-tête dont la valeur est une chaîne vide, les réponses 200 ne reçoivent pas no-store. Les location en expression régulière s'appliquent de haut en bas, la première qui correspond l'emporte, je l'ai donc placée avant la location des images existante, et j'ai ajouté une autre location du même type pour les fichiers statiques comme css, js et les polices.
En redemandant la même adresse après la correction, l'en-tête de réponse de la page en cache portait désormais un charset.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html; charset=utf-8curl -sI https://example.com/wp-content/uploads/2025/07/no-such-image.png | grep -i -E '^(HTTP|content-type|cache-control)'# Avant la correction : un 404 produit par WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Après la correction : un 404 renvoyé directement par nginx
HTTP/2 404
content-type: text/html
cache-control: no-storeLe 404 d'une image absente est passé d'une page WordPress qui produisait 319 058 octets en 1,01 seconde à la page par défaut de nginx, renvoyée en 0,015 seconde avec 555 octets. En ajoutant [Cache-Control: no-store] uniquement aux 404, le CDN ne stocke pas les 404 des images absentes. nginx n'ajoute pas un second charset aux réponses que PHP a déjà envoyées avec un charset, il n'apparaît donc qu'une fois dans l'en-tête de réponse.
| Requête vérifiée | Avant le correctif | Après la correction |
|---|---|---|
| Image absente | 319 058 o, 1,01 seconde (404 WordPress) | 555 o, 0,015 seconde (404 nginx) |
| Image au nom cassé | 1,05 seconde (404 WordPress) | 0,014 seconde (404 nginx) |
| En-tête de réponse du HTML en cache | text/html | text/html; charset=utf-8 |
| En-tête de réponse produit directement par PHP | text/html; charset=UTF-8 | Identique (charset une fois) |
| Image existante | 200 | Toujours 200 |
Ce traitement des 404 ne convient pas aux sites dont un plugin d'images génère les miniatures ou le WebP avec PHP après l'arrivée d'une requête. Comme nginx renvoie d'abord le 404, l'image n'est jamais générée. Si votre configuration crée les fichiers WebP à l'avance au moment de l'envoi, vous pouvez l'appliquer tel quel.
5️⃣ Pourquoi meta charset a été repoussé : Delay JS de WP Rocket
Même après l'ajout de charset à l'en-tête de réponse, le meta charset du HTML en cache reste à l'octet 15 199. Dans la page produite directement par PHP, cette même ligne était à l'octet 135.
Avec Delay JS de WP Rocket activé, la création du fichier de cache insère juste après head des scripts à exécution différée comme lazyload, puis une étape replace le meta charset tout au début de head. Cette étape applique une expression régulière à tout le document, si bien que sur un gros article d'environ 1,2 Mo elle dépasse la limite backtrack de PHP et échoue.
Quand l'expression régulière échoue, WP Rocket met en cache le HTML tel quel, avec les scripts insérés mais sans le meta charset déplacé, et le meta charset s'est ainsi retrouvé hors des 1 024 premiers octets. Dans une reproduction séparée avec les mêmes réglages PHP, le déplacement a échoué avec la limite backtrack à sa valeur par défaut de 1 000 000 et à 5 000 000, et a réussi à partir de 10 000 000.
Cette fois, j'ai résolu le problème en ajoutant charset à l'en-tête de réponse et je n'ai pas corrigé la position du meta charset. Relever la limite backtrack rétablit le comportement d'origine de WP Rocket, mais multiplie par dix la limite de toutes les expressions régulières exécutées par le même PHP, je ne le recommande donc pas.
🔢 FAQ & contenus recommandés
HTTP 404
Erreur HTTP 404 (erreur client) : causes et solutions
La page d'erreur HTTP 404, l'un des codes d'état HTTP, s'affiche lorsqu'une page est introuvable.
AdSense
Configurer ads.txt dans WordPress : résoudre l'avertissement AdSense
ads.txt est un fichier texte placé à la racine du site qui publie la liste des vendeurs autorisés à vendre les espaces publicitaires de ce domaine.
Cache WP Rocket
WP Rocket et URL en coréen : pourquoi nginx ne trouve pas le fichier de cache, et la solution avec njs
Pour les articles avec un slug en anglais, nginx sert directement le fichier de cache, mais pour les articles avec un slug en coréen, ce même fichier n'est servi qu'après une exécution de PHP, même s'il existe déjà.
ℹ️ Mention d'affiliation
Le contenu de ce site contient des liens d'affiliation. Lorsqu'un visiteur achète un produit ou un service via l'un de ces liens, le site perçoit une commission du vendeur. Le montant payé par l'acheteur(il baisse lors des promotions ↓), lui, n'augmente pas. Les prix, remises et stocks indiqués correspondent à la date de rédaction et peuvent différer ; vérifiez-les donc auprès du vendeur avant d'acheter. Les produits sont choisis et évalués selon nos propres critères, et les commissions n'influencent ni l'ordre de présentation ni le contenu des évaluations.