meta charset utf-8: błędy 404 przez koreańskie adresy obrazów uszkodzone przez crawler AdSense i zapełniony php-fpm

Środowisko testoweRodzina RHELAlmaLinux 9.8Konfiguracja z Nginx i Apachenginx serwuje statyczny HTML wtyczki cache bezpośrednioSprawdzono 2026-11-04
Część adresów, ścieżek i liczb to przykładowe wartości

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.

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

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

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

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

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

🌐 Mediapartners-GoogleŻądanie obrazu z uszkodzoną nazwą
location obrazów w nginxtry_files sprawdza plik na dysku
Plik istniejenginx od razu odpowiada 200
Brak plikuPrzekazanie do backendu, ostatniego elementu try_files
Apache → WordPressStrona 404 renderowana w PHP (mediana 1,4 sekundy)
Workery php-fpmJeden worker na żądanie
Limit puli (pm.max_children)Po zapełnieniu kolejne żądania czekają (499, 504)

Żą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-8
curl -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-store

Błą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 żądaniePrzed poprawkąPo poprawce
Nieistniejący obraz319 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 cachetext/htmltext/html; charset=utf-8
Nagłówek odpowiedzi wygenerowany bezpośrednio przez PHPtext/html; charset=UTF-8Bez zmian (charset raz)
Istniejący obraz200Nadal 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

Dokumentacja crawlerów Google podaje, że ustawienia indeksowania skierowane do Mediapartners-Google wpływają na produkty reklamowe, takie jak AdSense i Ad Manager. Ten crawler nie stosuje się do ogólnej reguły robots.txt [User-agent: *], więc aby go zablokować, trzeba wpisać jego nazwę osobno. Jeśli problemem są błędy 404 dla uszkodzonych adresów, lepiej, by serwer WWW od razu odpowiadał 404 na nieistniejące pliki, bo to nie wpływa na indeksowanie reklam.

Standard cache HTTP wymaga, aby przy zapisywaniu odpowiedzi zapisywać razem z nią otrzymane nagłówki odpowiedzi, więc kopie zapisane przez CDN przed poprawką nadal wychodzą jako [Content-Type: text/html] bez charset. Gdy wygasną i zostaną ponownie pobrane z serwera źródłowego albo gdy wyczyścisz w cache CDN tylko adres tego wpisu, przejdą na nowy nagłówek z charset.

Standard URL wymaga, aby znaki spoza ASCII w ścieżce adresu przed żądaniem zamieniać na kodowanie procentowe UTF-8, więc czytający, który poprawnie odczytał dokument jako UTF-8, żąda oryginalnej nazwy pliku. Nazwy plików tylko z liter łacińskich i cyfr pozostają takie same, nawet gdy dokument zostanie błędnie odczytany jako windows-1252, więc ich adresy nie psują się nawet na stronach, na których brakuje zarówno charset w nagłówku odpowiedzi, jak i meta charset.

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

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Prove your humanity: 4   +   7   =