meta charset utf-8 : des 404 sur des adresses d'images coréennes cassées par le robot AdSense et un php-fpm saturé

Environnement de testFamille RHELAlmaLinux 9.8Configuration Nginx et Apachenginx sert directement le HTML statique du plugin de cacheVérifié le 2026-11-04
Certaines adresses, certains chemins et certains chiffres sont des valeurs d'exemple

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.

🧭 meta charset utf-8 인코딩 결정 순서
설명 다이어그램
🔍 순서
📄 응답
🔖BOM(바이트 순서 표시)있으면 그 인코딩으로 정함없음없음없음
📨Content-Type 머리의 charset있으면 그 값으로 정함charset 없음charset 없음
🏷️meta charset 줄앞 1,024바이트 안에 있으면 그 값으로 정함1,024바이트 뒤로 밀려 찾지 못함
➜
🔎 읽는 쪽
🔤문서 인코딩찾은 값으로 읽음찾은 값으로 읽음찾은 값으로 읽음짐작하거나 기본값 windows-1252로 읽음
인코딩을 정할 때는 BOM(바이트 순서 표시)을 먼저 보며, BOM이 있으면 그 인코딩으로 읽습니다.
BOM이 없으면 다음은 응답 머리 Content-Type 뒤에 붙는 charset입니다.
그다음이 앞 1,024바이트 미리 읽기로 찾은 meta charset입니다. head 영역 안에 있더라도 1,024바이트 뒤로 밀린 meta charset은 찾지 못하는 읽는 쪽이 생깁니다.
셋 다 없으면 바이트 모양으로 짐작하거나 정해 둔 기본값으로 읽게 됩니다. 머리에 charset이 없고 meta charset이 1,024바이트 뒤에 있으면, 대부분의 locale에서 기본값인 windows-1252로 읽힐 수 있습니다.

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/html

Dans 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-8

La 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 -1
15199:<meta charset

Les 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 -2
203.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 404

Les 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.

🌐 Mediapartners-GoogleRequête d'image au nom cassé
location des images dans nginxtry_files vérifie le fichier sur le disque
Fichier présentnginx répond 200 directement
Fichier absentTransmis au backend, dernier élément de try_files
Apache → WordPressPage 404 rendue avec PHP (médiane 1,4 seconde)
Workers php-fpmUn worker par requête
Limite du pool (pm.max_children)Plein : les requêtes suivantes attendent (499, 504)

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 it

Les 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-8
curl -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-store

Le 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éeAvant le correctifAprès la correction
Image absente319 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 cachetext/htmltext/html; charset=utf-8
En-tête de réponse produit directement par PHPtext/html; charset=UTF-8Identique (charset une fois)
Image existante200Toujours 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

La documentation de Google sur ses robots d'exploration indique que les règles visant Mediapartners-Google affectent des produits publicitaires comme AdSense et Ad Manager. Ce robot ne suit pas la règle générale de robots.txt [User-agent: *], il faut donc le nommer à part pour le bloquer. Si le problème vient des 404 sur des adresses cassées, laisser le serveur web répondre directement par un 404 aux fichiers absents ne touche pas à l'exploration publicitaire.

Le standard de mise en cache HTTP impose de stocker les en-têtes de réponse reçus avec la réponse, donc les copies stockées par le CDN avant la correction continuent de partir en [Content-Type: text/html] sans charset. Une fois expirées et récupérées de nouveau sur le serveur d'origine, ou dès que vous purgez du cache du CDN la seule adresse de cet article, elles passent au nouvel en-tête avec charset.

Le standard URL impose de convertir les caractères non ASCII du chemin en encodage-pourcent UTF-8 avant la requête, donc un lecteur qui a bien lu le document en UTF-8 demande le nom de fichier d'origine. Les noms de fichiers composés uniquement de lettres latines et de chiffres restent identiques même si le document est mal lu en windows-1252, si bien que leurs adresses ne se cassent pas, même sur des pages où manquent à la fois le charset de l'en-tête de réponse et le meta charset.

ℹ️ 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.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Prouvez que vous êtes humain : 0   +   1   =