meta charset utf-8: 404's door Koreaanse afbeeldingsadressen die de AdSense-crawler kapotmaakte en een volle php-fpm-pool
Als de lezer de declaratie meta charset utf-8 niet vooraan in de HTML vindt en de responsheader ook geen charset heeft, moet hij de tekencodering van het document raden of een standaardwaarde gebruiken. Op een WordPress-server met Nginx en Apache stuurde de AdSense-crawler een golf verzoeken naar kapotte Koreaanse (niet-ASCII) afbeeldingsadressen, en de afhandeling van de 404's bezette alle PHP-workers. Na het volgen hiervan in de logs heb ik charset aan de responsheader van de gecachte pagina's toegevoegd en nginx ontbrekende statische bestanden direct met een 404 laten beantwoorden, zodat dezelfde verzoeken de workers niet opnieuw vullen.
1️⃣ meta charset utf-8: waarom het binnen de eerste 1.024 bytes moet staan
Een server kan de tekencodering van een document op twee plekken aangeven: de charset achter de responsheader [Content-Type], en de meta charset-regel in de HTML.
De HTML-standaard schrijft voor dat de declaratie van de tekencodering alleen binnen de eerste 1.024 bytes mag staan, en raadt de lezer aan alleen die eerste 1.024 bytes vooraf te lezen om ernaar te zoeken. Daarom kan een meta charset die voorbij 1.024 bytes is geschoven door sommige lezers worden gemist, ook al staat hij binnen head.
Om de codering te bepalen kijkt de lezer eerst naar de BOM (byte order mark), dan naar de charset in de responsheader en daarna naar de meta charset die gevonden wordt door de eerste 1.024 bytes vooraf te lezen. Ontbreken alle drie, dan gokt hij op basis van de bytes of gebruikt hij een vaste standaardwaarde, en in de tabel met standaardwaarden die de HTML-standaard voorstelt, is dat voor de meeste locale windows-1252.
Als je een UTF-8-document als windows-1252 leest, verandert één teken van meerdere bytes, zoals een Koreaanse letter, in meerdere andere tekens.
2️⃣ Koreaanse (niet-ASCII) afbeeldingsadressen die de AdSense-crawler Mediapartners-Google kapotmaakte
Toen ik de gecachte pagina van vóór de fix toetste aan de eerste 1.024 bytes, had de responsheader Content-Type geen charset en stond de meta charset-regel op byte 15.199. De gecachte pagina die de AdSense-crawler Mediapartners-Google kreeg, had dus op geen van beide plekken een codering die te vinden was.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlIn deze opzet sla je met een querystring achter het adres de gecachte pagina over en krijg je een respons die PHP direct heeft gemaakt, en die respons had wel een charset.
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8Op welke byte de meta charset in het cachebestand begint, zie je tot op de byte met [grep -b].
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charsetDe img-tags in hetzelfde cachebestand bevatten meertalige UTF-8-bestandsnamen (hier Koreaans) zoals ze waren, zonder procentcodering. Als deze HTML als windows-1252 wordt gelezen, wordt een naam als [대시보드-화면-예시.png] een reeks tekens als [대시보드-화면-예시.png], en als die tekens weer als UTF-8 worden procentgecodeerd en opgevraagd, zoekt de server een bestand dat niet bestaat.
Haal je de afbeeldings-404's van deze crawler uit het toegangslog, dan volgen de adressen met kapotte namen elkaar op.
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 404De adressen in het log waren byte voor byte gelijk aan de UTF-8-bytes van de oorspronkelijke bestandsnaam, gelezen als windows-1252-tekens en opnieuw als UTF-8 gecodeerd. Die dag telde het log 202 verzoeken naar op deze manier kapotte afbeeldingsadressen van deze crawler en 6 van één script (python-requests), en 0 van bezoekers.
Als je de kapotte naam [대시보드-화면-예시.png] door de UTF-8-converter haalt, wordt hij herkend als UTF-8-bytes die als windows-1252 zijn gelezen, en komt de oorspronkelijke naam [대시보드-화면-예시.png] terug.
3️⃣ Hoe 404's voor ontbrekende afbeeldingen php-fpm vulden
Omdat de afbeeldingen op de kapotte adressen niet op de server staan, gaf nginx de verzoeken volgens het laatste item van try_files in zijn location-regel voor afbeeldingen door aan Apache, en maakte WordPress de 404-pagina met PHP. Bij de 587 404's die die dag overdag werden gelogd, kostte elke 404-pagina zonder cache een mediaan van 1,4 seconde.
Verzoeken naar afbeeldingen met kapotte namen stapelden zich op tot iets meer dan 200 in de 3 minuten vanaf 20:40, en 133 daarvan eindigden in een 404. Elk verzoek krijgt een php-fpm-worker, en omdat er verzoeken bleven komen terwijl elke 404 4 tot 6 seconden duurde, bereikte de pool om 20:40:55 zijn limiet.
Het moment waarop de pool zijn limiet bereikt, komt in het foutlog van php-fpm terecht als php-fpm max_children-waarschuwing.
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 itVerzoeken die binnenkwamen nadat de pool vol was, wachtten op een vrije worker. De 404 die het langst wachtte, was na 58 seconden klaar, verzoeken die de crawler na 60 seconden wachten afbrak, werden gelogd als 499, en één verzoek dat geen antwoord van de backend kreeg als 504.
Het voorladen van de cache, de geplande taken (Action Scheduler) die op hetzelfde moment liepen en wp-cron, dat elke minuut draait, heb ik gecontroleerd en als oorzaak uitgesloten. Het voorladen liep de hele dag in hetzelfde lage tempo, de geplande taken verwerkten op dat moment alleen voorlaadtaken, en wp-cron draait met PHP op de opdrachtregel en gebruikt geen php-fpm-workers.
4️⃣ nginx charset utf-8 en direct 404 voor ontbrekende statische bestanden
Ik heb de ene regel [charset utf-8;] toegevoegd aan de nginx-location die de gecachte pagina's uitlevert, zodat de responsheader een charset heeft, en voor afbeeldingen en statische bestanden in de uploadmap geeft nginx nu direct een 404 als ze niet op schijf staan, in plaats van ze door te geven aan WordPress.
# http-blok: alleen voor 404-responses een Cache-Control-waarde instellen
map $status $up404_cc { 404 'no-store'; default ''; }
# location die de gecachte HTML direct uitlevert
location / {
try_files $cache_file @backend;
charset utf-8;
}
# Uploadmap: ontbrekende afbeeldingen krijgen direct een 404, zonder omweg via de backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}De map hoort in het http-blok, en omdat nginx een header met een lege waarde niet verstuurt, krijgen 200-responses geen no-store. Regex-locations worden van boven af toegepast en de eerste die past wint, dus ik heb deze vóór de bestaande location voor afbeeldingen gezet en voor statische bestanden zoals css, js en lettertypen nog een location van hetzelfde soort toegevoegd.
Toen ik na de fix hetzelfde adres opnieuw opvroeg, had de responsheader van de gecachte pagina een 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)'# Vóór de fix: een 404 gemaakt door WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Na de fix: een 404 die nginx direct teruggeeft
HTTP/2 404
content-type: text/html
cache-control: no-storeDe 404 voor een ontbrekende afbeelding ging van een WordPress-pagina die 319.058 bytes in 1,01 seconde maakte naar de standaardpagina van nginx, die in 0,015 seconde met 555 bytes terugkomt. Door [Cache-Control: no-store] alleen aan 404's te hangen, slaat het CDN geen 404's voor ontbrekende afbeeldingen op. Aan responses die PHP al met een charset heeft gestuurd, voegt nginx niet nog een charset toe, dus stond die maar één keer in de responsheader.
| Gecontroleerd verzoek | Vóór de oplossing | Na de fix |
|---|---|---|
| Ontbrekende afbeelding | 319.058 B, 1,01 seconde (WordPress-404) | 555 B, 0,015 seconde (nginx-404) |
| Afbeelding met kapotte naam | 1,05 seconde (WordPress-404) | 0,014 seconde (nginx-404) |
| Responsheader van gecachte HTML | text/html | text/html; charset=utf-8 |
| Responsheader direct gemaakt door PHP | text/html; charset=UTF-8 | Gelijk (charset één keer) |
| Bestaande afbeelding | 200 | Blijft 200 |
Deze 404-afhandeling past niet bij sites met afbeeldingsplugins die miniaturen of WebP pas na een verzoek met PHP maken. Omdat nginx eerst een 404 teruggeeft, wordt de afbeelding nooit gemaakt. Maakt jouw opzet WebP-bestanden al bij het uploaden aan, dan kun je dit zo overnemen.
5️⃣ Waarom meta charset naar achteren schoof: WP Rocket Delay JS
Ook na het toevoegen van charset aan de responsheader staat de meta charset in de gecachte HTML nog steeds op byte 15.199. In de pagina die PHP direct maakt, stond dezelfde regel op byte 135.
Met Delay JS van WP Rocket aan worden bij het maken van het cachebestand uitgestelde scripts zoals lazyload direct na head ingevoegd, waarna een stap de meta charset weer helemaal vooraan in head zet. Die stap past een regex toe op het hele document, dus bij een groot bericht van ongeveer 1,2 MB overschrijdt hij de backtrack-limiet van PHP en mislukt hij.
Als de regex mislukt, cachet WP Rocket de HTML zoals die is, met ingevoegde scripts maar zonder verplaatste meta charset, en daardoor schoof de meta charset buiten de eerste 1.024 bytes. In een aparte reproductie met dezelfde PHP-instellingen mislukte het verplaatsen met de backtrack-limiet op de standaardwaarde 1.000.000 en op 5.000.000, en lukte het vanaf 10.000.000.
Deze keer heb ik het opgelost door charset aan de responsheader toe te voegen en heb ik de positie van de meta charset niet aangepast. Een hogere backtrack-limiet brengt het oorspronkelijke gedrag van WP Rocket terug, maar vertienvoudigt de limiet voor elke regex in dezelfde PHP, dus dat raad ik niet aan.
🔢 FAQ & aanbevolen content
HTTP 404
HTTP 404-fout (clientfout): oorzaken en oplossing
De foutpagina HTTP 404, een van de HTTP-statuscodes, verschijnt wanneer een pagina niet gevonden kan worden.
AdSense
ads.txt instellen in WordPress: AdSense-waarschuwing oplossen
ads.txt is een tekstbestand in de hoofdmap van de site dat openbaar maakt welke geautoriseerde verkopers de advertentieruimte van dit domein mogen verkopen.
WP Rocket-cache
WP Rocket en Koreaanse URL's: waarom nginx het cachebestand niet vindt en de oplossing met njs
Bij berichten met een Engelse slug levert nginx het cachebestand direct uit, maar bij berichten met een Koreaanse slug gebeurt dat pas nadat PHP één keer heeft gedraaid, ook al bestaat hetzelfde cachebestand.
ℹ️ Affiliatemelding
De content van deze site bevat affiliatelinks. Koopt een bezoeker via een van deze links een product of dienst, dan ontvangt deze site een commissie van de verkoper. Het bedrag dat de koper betaalt (bij acties gaat het omlaag ↓) wordt daardoor niet hoger. De vermelde prijzen, kortingen en voorraad gelden op het moment van schrijven en kunnen afwijken; controleer ze daarom vóór je aankoop bij de verkoper. Producten worden gekozen en beoordeeld volgens onze eigen criteria, en commissies hebben geen invloed op de volgorde of de inhoud van de beoordelingen.