meta charset utf-8: 404 durch koreanische Bildadressen, die der AdSense-Crawler zerbrochen hat, und ein voller php-fpm-Pool

TestumgebungRHEL-FamilieAlmaLinux 9.8Aufbau mit Nginx und Apachenginx liefert das statische HTML des Cache-Plugins direkt ausGeprüft am 2026-11-04
Einige Adressen, Pfade und Zahlen sind Beispielwerte

Findet der Leser die Deklaration meta charset utf-8 nicht am Anfang des HTML und fehlt auch im Antwort-Header ein charset, muss er die Zeichenkodierung des Dokuments raten oder einen Standardwert nehmen. Auf einem WordPress-Server mit Nginx und Apache schickte der AdSense-Crawler gehäuft Anfragen an kaputte koreanische (Nicht-ASCII-)Bildadressen, und die 404-Verarbeitung belegte alle PHP-Worker. Nachdem ich das in den Logs verfolgt hatte, habe ich dem Antwort-Header der Cache-Seiten ein charset hinzugefügt und nginx fehlende statische Dateien direkt mit 404 beantworten lassen, damit dieselben Anfragen die Worker nicht wieder füllen.

1️⃣ meta charset utf-8: Warum es in den ersten 1.024 Bytes stehen muss

Der Server kann die Zeichenkodierung eines Dokuments an zwei Stellen angeben: im charset hinter dem Antwort-Header [Content-Type] und in der meta-charset-Zeile im HTML.

Der HTML-Standard schreibt vor, dass die Deklaration der Zeichenkodierung nur in den ersten 1.024 Bytes stehen darf, und empfiehlt dem Leser, nur diese ersten 1.024 Bytes vorab zu lesen und darin zu suchen. Deshalb übersehen manche Leser ein meta charset, das hinter 1.024 Bytes gerutscht ist, auch wenn es noch innerhalb von head steht.

Um die Kodierung festzulegen, prüft der Leser zuerst die BOM (Byte Order Mark), danach das charset im Antwort-Header und dann das meta charset, das beim Vorablesen der ersten 1.024 Bytes gefunden wird. Fehlen alle drei, rät er anhand der Bytes oder nutzt einen festgelegten Standardwert, und in der Standardwerttabelle, die der HTML-Standard vorschlägt, ist das für die meisten locale windows-1252.

Liest man ein UTF-8-Dokument als windows-1252, wird ein einzelnes Zeichen aus mehreren Bytes, etwa ein koreanischer Buchstabe, zu mehreren anderen Zeichen.

🧭 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️⃣ Koreanische (Nicht-ASCII-)Bildadressen, die der AdSense-Crawler Mediapartners-Google zerbrochen hat

Bei der Prüfung der Cache-Seite von vor der Korrektur nach dem Maßstab der ersten 1.024 Bytes hatte der Antwort-Header Content-Type kein charset, und die meta-charset-Zeile stand bei Byte 15.199. Die Cache-Seite, die der AdSense-Crawler Mediapartners-Google erhielt, hatte also an keiner der beiden Stellen eine auffindbare Kodierung.

curl -sI https://example.com/sample-post/ | grep -i '^content-type'
content-type: text/html

In diesem Aufbau überspringt ein Query-String hinter der Adresse die Cache-Seite, und es kommt eine direkt von PHP erzeugte Antwort, die tatsächlich ein charset trug.

curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'
content-type: text/html; charset=UTF-8

An welchem Byte das meta charset in der Cache-Datei beginnt, zeigt [grep -b] bytegenau an.

grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -1
15199:<meta charset

Die img-Tags in derselben Cache-Datei enthielten mehrsprachige UTF-8-Dateinamen (hier Koreanisch) unverändert, ohne Prozentkodierung. Wird dieses HTML als windows-1252 gelesen, wird aus einem Namen wie [대시보드-화면-예시.png] eine Zeichenfolge wie [대시보드-화면-예시.png], und wenn diese Zeichen wieder als UTF-8 prozentkodiert angefragt werden, sucht der Server eine Datei, die es nicht gibt.

Zieht man die Bild-404 dieses Crawlers aus dem Access-Log, folgen die Adressen mit kaputten Namen eine nach der anderen.

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

Die Adressen im Log stimmten Byte für Byte mit dem Wert überein, der entsteht, wenn man die UTF-8-Bytes des ursprünglichen Dateinamens als windows-1252-Zeichen liest und wieder als UTF-8 kodiert. Im Log dieses Tages kamen 202 Anfragen an so zerbrochene Bildadressen von diesem Crawler und 6 von einem einzelnen Skript (python-requests), von Besuchern kamen 0.

Wenn du den kaputten Namen [대시보드-화면-예시.png] mit dem UTF-8-Konverter zurückwandelst, wird er als UTF-8-Bytes erkannt, die als windows-1252 gelesen wurden, und der ursprüngliche Name [대시보드-화면-예시.png] erscheint.

3️⃣ Wie 404 für fehlende Bilder php-fpm füllten

Da die Bilder unter den kaputten Adressen auf dem Server nicht existieren, reichte nginx die Anfragen gemäß dem letzten Eintrag von try_files in seiner Bild-location an Apache weiter, und WordPress erzeugte die 404-Seite mit PHP. Bei den 587 404, die am selben Tag tagsüber protokolliert wurden, brauchte jede 404-Seite ohne Cache im Median 1,4 Sekunden.

🌐 Mediapartners-GoogleBildanfrage mit kaputtem Namen
Bild-location von nginxtry_files prüft die Datei auf der Festplatte
Datei vorhandennginx antwortet direkt mit 200
Datei fehltWeiter an das Backend, den letzten Eintrag von try_files
Apache → WordPress404-Seite mit PHP gerendert (Median 1,4 Sekunden)
php-fpm-WorkerEin Worker pro Anfrage
Pool-Obergrenze (pm.max_children)Voll: spätere Anfragen warten (499, 504)

Anfragen an Bilder mit kaputten Namen häuften sich in den 3 Minuten ab 20:40 auf gut 200, und 133 davon endeten mit 404. Jede Anfrage bekommt einen php-fpm-Worker, und weil weiter Anfragen kamen, während jede 404 4 bis 6 Sekunden dauerte, erreichte der Pool um 20:40:55 seine Obergrenze.

Der Moment, in dem der Pool seine Obergrenze erreicht, steht im Fehlerprotokoll von php-fpm als php-fpm max_children-Warnung.

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

Anfragen, die nach dem Volllaufen des Pools kamen, warteten auf einen freien Worker. Die am längsten wartende 404 war nach 58 Sekunden fertig, Anfragen, die der Crawler nach 60 Sekunden Warten abbrach, wurden als 499 protokolliert, und eine Anfrage ohne Antwort vom Backend als 504.

Das Vorladen des Caches, die geplanten Aufgaben (Action Scheduler), die zur selben Zeit liefen, und wp-cron, das jede Minute läuft, habe ich geprüft und als Ursache ausgeschlossen. Das Vorladen lief den ganzen Tag mit derselben niedrigen Rate, die geplanten Aufgaben verarbeiteten zu der Zeit nur Vorlade-Jobs, und wp-cron läuft mit Kommandozeilen-PHP und nutzt keine php-fpm-Worker.

4️⃣ nginx charset utf-8 und sofortiges 404 für fehlende statische Dateien

Ich habe der nginx-location, die die Cache-Seiten ausliefert, die eine Zeile [charset utf-8;] hinzugefügt, damit der Antwort-Header ein charset trägt, und für Bilder und statische Dateien im Upload-Ordner gibt nginx jetzt direkt 404 zurück, wenn sie nicht auf der Festplatte liegen, statt sie an WordPress weiterzureichen.

# http-Block: Cache-Control-Wert nur für 404-Antworten festlegen
map $status $up404_cc { 404 'no-store'; default ''; }

# location, die das Cache-HTML direkt ausliefert
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# Upload-Ordner: fehlende Bilder bekommen direkt 404, ohne Umweg über das Backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
    try_files $uri =404;
    add_header Cache-Control $up404_cc always;
}

Die map gehört in den http-Block, und weil nginx einen Header mit leerem Wert gar nicht sendet, bekommen 200-Antworten kein no-store. Regex-locations werden von oben geprüft, und die erste passende gilt, deshalb habe ich sie vor die bestehende Bild-location gesetzt und für statische Dateien wie css, js und Schriften eine weitere location derselben Art ergänzt.

Nach der Korrektur trug der Antwort-Header der Cache-Seite bei derselben Adresse ein 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)'
# Vor der Korrektur: eine von WordPress erzeugte 404
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Nach der Korrektur: eine direkt von nginx zurückgegebene 404
HTTP/2 404
content-type: text/html
cache-control: no-store

Die 404 für ein fehlendes Bild wechselte von einer WordPress-Seite, die 319.058 Bytes in 1,01 Sekunden erzeugte, zur Standardseite von nginx mit 555 Bytes in 0,015 Sekunden. Weil [Cache-Control: no-store] nur an 404 hängt, speichert das CDN keine 404 für fehlende Bilder. An Antworten, die PHP schon mit charset geschickt hat, hängt nginx kein zweites charset an, deshalb stand es nur einmal im Antwort-Header.

Geprüfte AnfrageVor der KorrekturNach der Korrektur
Fehlendes Bild319.058 B, 1,01 Sekunden (WordPress-404)555 B, 0,015 Sekunden (nginx-404)
Bild mit kaputtem Namen1,05 Sekunden (WordPress-404)0,014 Sekunden (nginx-404)
Antwort-Header des Cache-HTMLtext/htmltext/html; charset=utf-8
Antwort-Header direkt von PHP erzeugttext/html; charset=UTF-8Gleich (charset einmal)
Vorhandenes Bild200Weiter 200

Für Websites mit Bild-Plugins, die Vorschaubilder oder WebP erst nach einer Anfrage mit PHP erzeugen, passt diese 404-Behandlung nicht. Weil nginx zuerst 404 zurückgibt, wird das Bild nie erzeugt. Wenn dein Aufbau WebP schon beim Hochladen als Datei anlegt, kannst du sie unverändert übernehmen.

5️⃣ Warum meta charset nach hinten rutschte: WP Rocket Delay JS

Auch nach dem charset im Antwort-Header steht das meta charset im Cache-HTML weiterhin bei Byte 15.199. In der direkt von PHP erzeugten Seite stand dieselbe Zeile bei Byte 135.

Ist Delay JS von WP Rocket eingeschaltet, fügt es beim Erstellen der Cache-Datei direkt nach head verzögerte Skripte wie lazyload ein und verschiebt danach in einem eigenen Schritt das meta charset wieder an den Anfang von head. Dieser Schritt wendet eine Regex auf das ganze Dokument an, daher überschreitet er bei einem großen Beitrag von etwa 1,2 MB das backtrack-Limit von PHP und scheitert.

Scheitert die Regex, cacht WP Rocket das HTML so, wie es ist, mit eingefügten Skripten, aber ohne verschobenes meta charset, und dadurch rutschte das meta charset aus den ersten 1.024 Bytes heraus. In einer separaten Nachstellung mit denselben PHP-Einstellungen scheiterte das Verschieben beim backtrack-Limit auf dem Standardwert 1.000.000 und auf 5.000.000 und gelang ab 10.000.000.

Diesmal habe ich das Problem mit dem charset im Antwort-Header gelöst und die Position des meta charset nicht korrigiert. Ein höheres backtrack-Limit bringt das ursprüngliche Verhalten von WP Rocket zurück, verzehnfacht aber das Limit für jede Regex im selben PHP, deshalb empfehle ich es nicht.

🔢 FAQ & empfohlene Inhalte

Laut der Crawler-Dokumentation von Google wirken sich Crawling-Regeln für Mediapartners-Google auf Werbeprodukte wie AdSense und Ad Manager aus. Dieser Crawler folgt der allgemeinen robots.txt-Regel [User-agent: *] nicht, deshalb musst du ihn zum Sperren eigens namentlich aufführen. Wenn die 404 für kaputte Adressen das Problem sind, lässt du fehlende Dateien besser direkt vom Webserver mit 404 beantworten, das Crawling für Anzeigen bleibt dann unberührt.

Der HTTP-Caching-Standard schreibt vor, beim Speichern einer Antwort die empfangenen Antwort-Header mitzuspeichern, daher gehen Kopien, die das CDN vor der Korrektur gespeichert hat, weiter als [Content-Type: text/html] ohne charset raus. Sobald sie ablaufen und neu vom Ursprungsserver geholt werden oder du nur die Adresse dieses Beitrags im CDN-Cache leerst, wechseln sie zum neuen Header mit charset.

Der URL-Standard schreibt vor, Nicht-ASCII-Zeichen im Pfad vor der Anfrage in UTF-8-Prozentkodierung umzuwandeln, daher fragt ein Leser, der das Dokument korrekt als UTF-8 gelesen hat, den ursprünglichen Dateinamen an. Dateinamen nur aus lateinischen Buchstaben und Ziffern bleiben gleich, auch wenn das Dokument fälschlich als windows-1252 gelesen wird, daher brechen ihre Adressen auch auf Seiten nicht, denen sowohl das charset im Antwort-Header als auch das meta charset fehlt.

ℹ️ Affiliate-Hinweis
Die Inhalte dieser Website enthalten Affiliate-Links. Kauft ein Besucher über einen dieser Links ein Produkt oder eine Dienstleistung, erhält die Website vom Verkäufer eine Provision. Der Betrag, den der Käufer zahlt(bei Aktionsrabatten sinkt er ↓), steigt dadurch nicht. Angegebene Preise, Rabatte und Lagerbestände entsprechen dem Stand bei Erstellung und können abweichen; prüfe sie daher vor dem Kauf beim Verkäufer. Produkte werden nach eigenen Kriterien ausgewählt und bewertet, und Provisionen beeinflussen weder die Reihenfolge noch den Inhalt der Bewertungen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Beweisen Sie, dass Sie ein Mensch sind: 7   +   6   =