meta charset utf-8: 404's door Koreaanse afbeeldingsadressen die de AdSense-crawler kapotmaakte en een volle php-fpm-pool

TestomgevingRHEL-familieAlmaLinux 9.8Opzet met Nginx en Apachenginx levert de statische HTML van de cacheplugin direct uitGecontroleerd op 2026-11-04
Sommige adressen, paden en getallen zijn voorbeeldwaarden

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.

🧭 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️⃣ 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/html

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

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

De 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 -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

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

🌐 Mediapartners-GoogleAfbeeldingsverzoek met kapotte naam
location voor afbeeldingen in nginxtry_files controleert het bestand op schijf
Bestand aanwezignginx antwoordt direct met 200
Bestand ontbreektDoor naar de backend, het laatste item van try_files
Apache → WordPress404-pagina gerenderd met PHP (mediaan 1,4 seconde)
php-fpm-workersEén worker per verzoek
Poollimiet (pm.max_children)Vol: latere verzoeken wachten (499, 504)

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 it

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

De 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 verzoekVóór de oplossingNa de fix
Ontbrekende afbeelding319.058 B, 1,01 seconde (WordPress-404)555 B, 0,015 seconde (nginx-404)
Afbeelding met kapotte naam1,05 seconde (WordPress-404)0,014 seconde (nginx-404)
Responsheader van gecachte HTMLtext/htmltext/html; charset=utf-8
Responsheader direct gemaakt door PHPtext/html; charset=UTF-8Gelijk (charset één keer)
Bestaande afbeelding200Blijft 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

Volgens de crawlerdocumentatie van Google hebben crawlinstellingen voor Mediapartners-Google invloed op advertentieproducten zoals AdSense en Ad Manager. Deze crawler volgt de algemene robots.txt-regel [User-agent: *] niet, dus om hem te blokkeren moet je hem apart bij naam noemen. Als de 404's voor kapotte adressen het probleem zijn, laat je ontbrekende bestanden beter direct door de webserver met een 404 beantwoorden, dan blijft het crawlen voor advertenties ongemoeid.

De HTTP-cachingstandaard schrijft voor dat bij het opslaan van een respons ook de ontvangen responsheaders worden opgeslagen, dus kopieën die het CDN vóór de fix heeft opgeslagen, gaan nog steeds uit als [Content-Type: text/html] zonder charset. Zodra ze verlopen en opnieuw van de originele server worden gehaald, of zodra je alleen het adres van dat bericht uit de CDN-cache wist, krijgen ze de nieuwe header met charset.

De URL-standaard schrijft voor dat niet-ASCII-tekens in het pad vóór het verzoek worden omgezet in UTF-8-procentcodering, dus een lezer die het document correct als UTF-8 heeft gelezen, vraagt de oorspronkelijke bestandsnaam op. Bestandsnamen met alleen Latijnse letters en cijfers blijven gelijk, ook als het document per ongeluk als windows-1252 wordt gelezen, dus hun adressen breken niet, zelfs niet op pagina's waar zowel de charset in de responsheader als de meta charset ontbreekt.

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

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Laat zien dat je geen machine bent: 6   +   3   =