meta charset utf-8: błędy 404 przez koreańskie adresy obrazów uszkodzone przez crawler AdSense i zapełniony php-fpm
Jeśli czytający nie znajdzie deklaracji meta charset utf-8 na początku HTML, a nagłówek odpowiedzi też nie ma charset, musi zgadywać kodowanie znaków dokumentu albo odczytać go z wartością domyślną. Na serwerze WordPress z Nginx i Apache crawler AdSense wysłał falę żądań do uszkodzonych koreańskich (spoza ASCII) adresów obrazów, a obsługa błędów 404 zajęła wszystkie workery PHP. Po prześledzeniu tego w logach do nagłówka odpowiedzi stron z cache dodano charset, a nginx od razu odpowiada 404 na nieistniejące pliki statyczne, więc te same żądania nie zapełniają już workerów.
1️⃣ meta charset utf-8: dlaczego musi być w pierwszych 1024 bajtach
Serwer może podać kodowanie znaków dokumentu w dwóch miejscach: w charset dopisanym za nagłówkiem odpowiedzi [Content-Type] oraz w wierszu meta charset zapisanym w HTML.
Standard HTML wymaga, aby deklaracja kodowania znaków pojawiała się tylko w pierwszych 1024 bajtach, i zaleca czytającym, by wstępnie odczytywali tylko te pierwsze 1024 bajty i tam jej szukali. Dlatego nawet wewnątrz head niektórzy czytający nie znajdą meta charset przesuniętego za 1024 bajty.
Przy wyborze kodowania czytający najpierw sprawdza BOM (znacznik kolejności bajtów), potem charset w nagłówku odpowiedzi, a następnie meta charset znaleziony przy wstępnym odczycie pierwszych 1024 bajtów. Jeśli nie ma żadnego z trzech, zgaduje na podstawie układu bajtów albo używa ustalonej wartości domyślnej, a w tabeli wartości domyślnych proponowanej przez standard HTML większość locale ma windows-1252.
Gdy odczytasz dokument UTF-8 jako windows-1252, jeden znak złożony z kilku bajtów, na przykład koreańska litera, zmienia się w kilka innych znaków.
2️⃣ Koreańskie (spoza ASCII) adresy obrazów uszkodzone przez crawler AdSense Mediapartners-Google
Sprawdzając stronę z cache sprzed poprawki pod kątem pierwszych 1024 bajtów, nagłówek odpowiedzi Content-Type nie miał charset, a wiersz meta charset znajdował się na bajcie 15 199. Strona z cache, którą otrzymał crawler AdSense Mediapartners-Google, nie miała więc kodowania możliwego do znalezienia w żadnym z tych dwóch miejsc.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlW tej konfiguracji dodanie ciągu zapytania do adresu omija stronę z cache i zwraca odpowiedź wygenerowaną bezpośrednio przez PHP, a ta odpowiedź miała charset.
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8Miejsce, w którym w pliku cache zaczyna się meta charset, sprawdzisz co do bajta za pomocą [grep -b].
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charsetZnaczniki img w tym samym pliku cache zawierały wielojęzyczne nazwy plików w UTF-8 (tutaj po koreańsku) w niezmienionej postaci, bez kodowania procentowego. Gdy ten HTML zostanie odczytany jako windows-1252, nazwa taka jak [대시보드-화면-예시.png] zmienia się w znaki w rodzaju [대시보드-화면-예시.png], a gdy te znaki zostaną ponownie zakodowane procentowo jako UTF-8 i zażądane, serwer szuka pliku, którego nie ma.
Gdy wyciągniesz z logu dostępu błędy 404 obrazów otrzymane przez ten crawler, adresy z uszkodzonymi nazwami pojawiają się jeden za drugim.
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 404Adresy zapisane w logu były bajt w bajt identyczne z wartością uzyskaną przez odczytanie bajtów UTF-8 oryginalnej nazwy pliku jako znaków windows-1252 i ponowne zakodowanie ich w UTF-8. W logu z tego dnia żądań do adresów obrazów uszkodzonych w ten sam sposób było 202 od tego crawlera i 6 od jednego skryptu (python-requests), a od odwiedzających 0.
Gdy przepuścisz uszkodzoną nazwę [대시보드-화면-예시.png] przez narzędzie Konwerter UTF-8, zostanie ona rozpoznana jako bajty UTF-8 odczytane jako windows-1252 i pojawi się oryginalna nazwa [대시보드-화면-예시.png].
3️⃣ Jak błędy 404 nieistniejących obrazów zapełniły php-fpm
Ponieważ obrazów pod uszkodzonymi adresami nie ma na serwerze, nginx zgodnie z ostatnim elementem try_files w regule location dla obrazów przekazał żądania do Apache, a WordPress wygenerował stronę 404 w PHP. Spośród 587 błędów 404 zapisanych za dnia tego samego dnia każda strona 404 bez cache zajmowała w medianie 1,4 sekundy.
Żądania obrazów z uszkodzonymi nazwami skumulowały się do nieco ponad 200 w ciągu 3 minut od 20:40, a 133 z nich zakończyły się błędem 404. Każde żądanie dostaje jednego workera php-fpm, a ponieważ żądania wciąż napływały, gdy każdy błąd 404 trwał od 4 do 6 sekund, pula osiągnęła limit o 20:40:55.
Moment, w którym pula osiąga limit, zostaje w logu błędów php-fpm: Ostrzeżenie 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Żądania, które przyszły po zapełnieniu puli, czekały na wolnego workera. Najdłużej czekający błąd 404 zakończył się po 58 sekundach, żądania przerwane przez crawler po 60 sekundach czekania zapisano jako 499, a jedno żądanie bez odpowiedzi z backendu jako 504.
Wstępne ładowanie cache, zadania zaplanowane (Action Scheduler) działające w tym samym czasie i wp-cron uruchamiany co minutę zostały sprawdzone i wykluczone jako przyczyna. Wstępne ładowanie działało przez cały dzień w tym samym niskim tempie, zadania zaplanowane w tym czasie obsługiwały tylko zadania wstępnego ładowania, a wp-cron działa w PHP z wiersza poleceń i nie używa workerów php-fpm.
4️⃣ nginx charset utf-8 i natychmiastowe 404 dla nieistniejących plików statycznych
Do location nginx, która serwuje strony z cache, dodano jeden wiersz [charset utf-8;], aby nagłówek odpowiedzi miał charset, a dla obrazów i plików statycznych w folderze uploadów, jeśli nie ma ich na dysku, nginx zwraca teraz od razu 404 zamiast przekazywać je do WordPress.
# Blok http: wartość Cache-Control ustawiana tylko dla odpowiedzi 404
map $status $up404_cc { 404 'no-store'; default ''; }
# location, która serwuje HTML z cache bezpośrednio
location / {
try_files $cache_file @backend;
charset utf-8;
}
# Folder uploadów: nieistniejące obrazy dostają od razu 404 bez przekazywania do backendu
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}map umieszcza się w bloku http, a ponieważ nginx nie wysyła nagłówka, którego wartość jest pustym ciągiem, odpowiedzi 200 nie dostają no-store. Location z wyrażeniem regularnym są stosowane od góry i wygrywa pierwsza pasująca, dlatego umieszczono ją przed istniejącą location obrazów i dodano jeszcze jedną location tego samego rodzaju dla plików statycznych, takich jak css, js i czcionki.
Po poprawce ponowne żądanie tego samego adresu pokazało, że nagłówek odpowiedzi strony z cache ma już 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)'# Przed poprawką: 404 wygenerowane przez WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Po poprawce: 404 zwrócone bezpośrednio przez nginx
HTTP/2 404
content-type: text/html
cache-control: no-storeBłąd 404 dla nieistniejącego obrazu zmienił się ze strony WordPress, która generowała 319 058 bajtów w 1,01 sekundy, na domyślną stronę nginx zwracaną w 0,015 sekundy przy 555 bajtach. Dzięki dodaniu [Cache-Control: no-store] tylko do odpowiedzi 404 CDN nie zapisuje błędów 404 nieistniejących obrazów. Do odpowiedzi, które PHP wysłał już z charset, nginx nie dodaje charset ponownie, więc w nagłówku odpowiedzi pojawił się tylko raz.
| Sprawdzone żądanie | Przed poprawką | Po poprawce |
|---|---|---|
| Nieistniejący obraz | 319 058 B, 1,01 sekundy (404 WordPress) | 555 B, 0,015 sekundy (404 nginx) |
| Obraz z uszkodzoną nazwą | 1,05 sekundy (404 WordPress) | 0,014 sekundy (404 nginx) |
| Nagłówek odpowiedzi HTML z cache | text/html | text/html; charset=utf-8 |
| Nagłówek odpowiedzi wygenerowany bezpośrednio przez PHP | text/html; charset=UTF-8 | Bez zmian (charset raz) |
| Istniejący obraz | 200 | Nadal 200 |
Ta obsługa 404 nie pasuje do witryn z wtyczkami do obrazów, które generują miniatury lub WebP w PHP dopiero po nadejściu żądania. Ponieważ nginx najpierw zwraca 404, obraz nigdy nie zostanie wygenerowany. Jeśli twoja konfiguracja tworzy pliki WebP z wyprzedzeniem podczas przesyłania, możesz zastosować ją bez zmian.
5️⃣ Dlaczego meta charset przesunął się w dół: WP Rocket Delay JS
Nawet po dodaniu charset do nagłówka odpowiedzi meta charset w HTML z cache nadal jest na bajcie 15 199. Na stronie wygenerowanej bezpośrednio przez PHP ten sam wiersz był na bajcie 135.
Gdy w WP Rocket włączone jest Delay JS, przy tworzeniu pliku cache zaraz po head wstawiane są skrypty z opóźnionym wykonaniem, takie jak lazyload, a potem osobny krok przenosi meta charset z powrotem na sam początek head. Ten krok stosuje wyrażenie regularne do całego dokumentu, więc przy dużym wpisie o rozmiarze około 1,2 MB przekracza limit backtrack w PHP i kończy się niepowodzeniem.
Gdy wyrażenie regularne zawiedzie, WP Rocket zapisuje w cache HTML w niezmienionej postaci, ze wstawionymi skryptami, ale bez przeniesionego meta charset, i w efekcie meta charset wypadł poza pierwsze 1024 bajty. W osobnym odtworzeniu z tymi samymi ustawieniami PHP przeniesienie nie udało się przy limicie backtrack równym domyślnym 1 000 000 i 5 000 000, a udało się od 10 000 000.
Tym razem problem rozwiązano przez dodanie charset do nagłówka odpowiedzi, a położenia meta charset nie poprawiano. Podniesienie limitu backtrack przywraca pierwotne działanie WP Rocket, ale dziesięciokrotnie podnosi limit wszystkich wyrażeń regularnych działających w tym samym PHP, dlatego tego nie polecam.
🔢 FAQ & polecane treści
HTTP 404
Błąd HTTP 404 (błąd klienta): przyczyny i rozwiązanie
Strona błędu HTTP 404, jednego z kodów stanu HTTP, pojawia się, gdy nie można znaleźć strony.
AdSense
Konfiguracja ads.txt w WordPress: jak rozwiązać ostrzeżenie AdSense
ads.txt to plik tekstowy umieszczany w katalogu głównym witryny, który publicznie wymienia autoryzowanych sprzedawców uprawnionych do sprzedaży powierzchni reklamowej tej domeny.
Cache WP Rocket
WP Rocket i koreańskie adresy URL: dlaczego nginx nie znajduje pliku cache i rozwiązanie z njs
W przypadku wpisów ze slugiem po angielsku nginx serwuje plik cache bezpośrednio, ale w przypadku wpisów ze slugiem po koreańsku ten sam plik cache jest serwowany dopiero po jednokrotnym uruchomieniu PHP, nawet jeśli już istnieje.
ℹ️ Informacja o linkach partnerskich
Treści tej witryny zawierają linki partnerskie. Jeśli odwiedzający kupi produkt lub usługę przez taki link, witryna otrzymuje od sprzedawcy prowizję. Kwota płacona przy tym przez kupującego (podczas promocji spada↓) nie wzrasta. Podane ceny, rabaty i dostępność odnoszą się do chwili pisania i mogą różnić się od stanu faktycznego, dlatego przed zakupem sprawdź je u sprzedawcy. Produkty wybieramy i oceniamy według własnych kryteriów, a prowizja nie wpływa ani na kolejność prezentacji, ani na treść ocen.