meta charset utf-8: 404 da indirizzi di immagini in coreano rotti dal crawler di AdSense e php-fpm pieno

Ambiente di testFamiglia RHELAlmaLinux 9.8Configurazione con Nginx e Apachenginx serve direttamente l'HTML statico del plugin di cacheVerificato il 2026-11-04
Alcuni indirizzi, percorsi e numeri sono valori di esempio

Se il lettore non trova la dichiarazione meta charset utf-8 all'inizio dell'HTML e anche l'intestazione di risposta non ha charset, deve indovinare la codifica dei caratteri del documento o leggerlo con un valore predefinito. Su un server WordPress con Nginx e Apache, il crawler di AdSense ha inviato una raffica di richieste verso indirizzi di immagini in coreano (non ASCII) rotti, e la gestione dei 404 ha occupato tutti i worker PHP. Dopo averlo seguito nei log, ho aggiunto charset all'intestazione di risposta delle pagine in cache e ho fatto rispondere nginx direttamente con un 404 ai file statici inesistenti, così le stesse richieste non riempiono più i worker.

1️⃣ meta charset utf-8: perché deve stare nei primi 1.024 byte

Il server può indicare la codifica dei caratteri del documento in due punti: il charset aggiunto dopo l'intestazione di risposta [Content-Type] e la riga meta charset scritta dentro l'HTML.

Lo standard HTML stabilisce che la dichiarazione della codifica compaia solo nei primi 1.024 byte, e consiglia al lettore di leggere in anticipo solo quei primi 1.024 byte per trovarla. Per questo, anche dentro head, un meta charset spinto oltre 1.024 byte può sfuggire ad alcuni lettori.

Per decidere la codifica, il lettore guarda prima il BOM (byte order mark), poi il charset dell'intestazione di risposta e infine il meta charset trovato con la lettura preliminare dei primi 1.024 byte. Se non c'è nessuno dei tre, ipotizza in base alla forma dei byte o usa un valore predefinito, e nella tabella dei valori predefiniti proposta dallo standard HTML la maggior parte dei locale ha windows-1252.

Se leggi un documento UTF-8 come windows-1252, un singolo carattere di più byte, come una lettera coreana, diventa diversi caratteri differenti.

🧭 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️⃣ Indirizzi di immagini in coreano (non ASCII) rotti dal crawler di AdSense Mediapartners-Google

Controllando la pagina in cache di prima della correzione con il criterio dei primi 1.024 byte, l'intestazione di risposta Content-Type non aveva charset e la riga meta charset si trovava al byte 15.199. Quindi la pagina in cache ricevuta dal crawler di AdSense Mediapartners-Google non aveva una codifica rintracciabile in nessuno dei due punti.

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

In questa configurazione, se aggiungi una query string all'indirizzo, la pagina in cache viene saltata e arriva una risposta generata direttamente da PHP, e quella risposta aveva il charset.

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

Con [grep -b] vedi, byte per byte, in quale posizione del file di cache inizia il meta charset.

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

I tag img dello stesso file di cache contenevano nomi di file multilingue in UTF-8 (qui in coreano) così com'erano, senza codifica percentuale. Se questo HTML viene letto come windows-1252, un nome come [대시보드-화면-예시.png] diventa caratteri come [대시보드-화면-예시.png], e se quei caratteri vengono di nuovo codificati in percentuale come UTF-8 e richiesti, il server cerca un file che non esiste.

Se estrai dal log di accesso i 404 delle immagini ricevuti da questo crawler, compaiono uno dopo l'altro indirizzi con nomi rotti.

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

Gli indirizzi rimasti nel log erano identici, byte per byte, al valore ottenuto leggendo i byte UTF-8 del nome originale come caratteri windows-1252 e codificandoli di nuovo in UTF-8. Nel log di quel giorno le richieste a indirizzi di immagini rotti nello stesso modo erano 202 da questo crawler e 6 da un singolo script (python-requests), e 0 dai visitatori.

Se passi il nome rotto [대시보드-화면-예시.png] con lo strumento Convertitore UTF-8, viene riconosciuto come byte UTF-8 letti come windows-1252 e ricompare il nome originale [대시보드-화면-예시.png].

3️⃣ Come i 404 delle immagini inesistenti hanno riempito php-fpm

Poiché le immagini degli indirizzi rotti non esistono sul server, nginx ha passato le richieste ad Apache seguendo l'ultima voce di try_files nella sua regola location per le immagini, e WordPress ha generato la pagina 404 con PHP. Guardando i 587 404 registrati di giorno in quella stessa data, ogni pagina 404 senza cache ha richiesto una mediana di 1,4 secondi.

🌐 Mediapartners-GoogleRichiesta di immagine con nome rotto
location delle immagini in nginxtry_files controlla il file su disco
File presentenginx risponde 200 direttamente
File assentePassa al backend, ultima voce di try_files
Apache → WordPressPagina 404 generata con PHP (mediana 1,4 secondi)
Worker di php-fpmUn worker per richiesta
Limite del pool (pm.max_children)Pieno: le richieste successive aspettano (499, 504)

Le richieste di immagini con nomi rotti si sono accumulate fino a poco più di 200 nei 3 minuti dalle 20:40, e 133 di esse sono finite con un 404. Ogni richiesta riceve un worker di php-fpm, e poiché le richieste continuavano ad arrivare mentre ogni 404 richiedeva da 4 a 6 secondi, il pool ha raggiunto il limite alle 20:40:55.

Il momento in cui il pool raggiunge il limite resta nel log degli errori di php-fpm: Avviso 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

Le richieste arrivate dopo il riempimento del pool hanno aspettato un worker libero. Il 404 che ha aspettato di più è finito dopo 58 secondi, le richieste che il crawler ha interrotto dopo 60 secondi di attesa sono state registrate come 499, e una richiesta rimasta senza risposta dal backend come 504.

Il precaricamento della cache, le attività pianificate (Action Scheduler) in esecuzione alla stessa ora e wp-cron, eseguito ogni minuto, sono stati controllati ed esclusi come causa. Il precaricamento girava tutto il giorno alla stessa velocità bassa, le attività pianificate a quell'ora elaboravano solo lavori di precaricamento, e wp-cron viene eseguito con PHP da riga di comando e non usa i worker di php-fpm.

4️⃣ nginx charset utf-8 e 404 immediato per i file statici inesistenti

Ho aggiunto la riga [charset utf-8;] alla location di nginx che serve le pagine in cache, così l'intestazione di risposta ha il charset, e per le immagini e i file statici della cartella degli upload, se non sono su disco, ora nginx restituisce direttamente un 404 invece di passarli a WordPress.

# Blocco http: imposta un valore Cache-Control solo per le risposte 404
map $status $up404_cc { 404 'no-store'; default ''; }

# location che serve direttamente l'HTML in cache
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# Cartella degli upload: le immagini inesistenti ricevono subito un 404 senza passare dal backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
    try_files $uri =404;
    add_header Cache-Control $up404_cc always;
}

La map va nel blocco http, e poiché nginx non invia un'intestazione il cui valore è una stringa vuota, le risposte 200 non ricevono no-store. Le location con espressione regolare si applicano dall'alto e vale la prima che corrisponde, quindi l'ho messa prima della location delle immagini esistente e ho aggiunto un'altra location dello stesso tipo per i file statici come css, js e font.

Richiedendo di nuovo lo stesso indirizzo dopo la correzione, l'intestazione di risposta della pagina in cache aveva il 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)'
# Prima della correzione: un 404 generato da WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Dopo la correzione: un 404 restituito direttamente da nginx
HTTP/2 404
content-type: text/html
cache-control: no-store

Il 404 di un'immagine inesistente è passato da una pagina di WordPress che generava 319.058 byte in 1,01 secondi alla pagina predefinita di nginx, restituita in 0,015 secondi con 555 byte. Aggiungendo [Cache-Control: no-store] solo ai 404, la CDN non salva i 404 delle immagini inesistenti. nginx non aggiunge di nuovo charset alle risposte che PHP ha già inviato con il charset, quindi è comparso una sola volta nell'intestazione di risposta.

Richiesta controllataPrima della correzioneDopo la correzione
Immagine inesistente319.058 B, 1,01 secondi (404 di WordPress)555 B, 0,015 secondi (404 di nginx)
Immagine con nome rotto1,05 secondi (404 di WordPress)0,014 secondi (404 di nginx)
Intestazione di risposta dell'HTML in cachetext/htmltext/html; charset=utf-8
Intestazione di risposta generata direttamente da PHPtext/html; charset=UTF-8Uguale (charset una volta)
Immagine esistente200Resta 200

Questa gestione dei 404 non va bene per i siti con plugin per immagini che generano miniature o WebP con PHP dopo l'arrivo di una richiesta. Poiché nginx restituisce prima il 404, l'immagine non viene mai generata. Se la tua configurazione crea in anticipo i file WebP al momento del caricamento, puoi applicarla così com'è.

5️⃣ Perché meta charset è finito più in basso: Delay JS di WP Rocket

Anche dopo aver aggiunto charset all'intestazione di risposta, il meta charset dell'HTML in cache resta al byte 15.199. Nella pagina generata direttamente da PHP, la stessa riga era al byte 135.

Con Delay JS di WP Rocket attivo, quando viene creato il file di cache vengono inseriti subito dopo head script a esecuzione ritardata come lazyload, e poi un passaggio riporta il meta charset all'inizio di head. Questo passaggio applica un'espressione regolare all'intero documento, quindi su un articolo grande di circa 1,2 MB supera il limite di backtrack di PHP e fallisce.

Quando l'espressione regolare fallisce, WP Rocket mette in cache l'HTML così com'è, con gli script inseriti ma senza aver spostato il meta charset, e di conseguenza il meta charset è finito fuori dai primi 1.024 byte. In una riproduzione separata con le stesse impostazioni di PHP, lo spostamento è fallito con il limite di backtrack al valore predefinito di 1.000.000 e a 5.000.000, ed è riuscito da 10.000.000 in su.

Questa volta ho risolto aggiungendo charset all'intestazione di risposta e non ho corretto la posizione del meta charset. Alzare il limite di backtrack ripristina il comportamento originale di WP Rocket, ma decuplica il limite di tutte le espressioni regolari eseguite nello stesso PHP, quindi non lo consiglio.

🔢 FAQ & contenuti consigliati

La documentazione di Google sui crawler spiega che le impostazioni di scansione rivolte a Mediapartners-Google influiscono su prodotti pubblicitari come AdSense e Ad Manager. Questo crawler non segue la regola generale di robots.txt [User-agent: *], quindi per bloccarlo devi indicarlo per nome a parte. Se il problema sono i 404 degli indirizzi rotti, far rispondere il server web direttamente con un 404 ai file inesistenti non tocca la scansione pubblicitaria.

Lo standard di caching HTTP stabilisce che, quando si salva una risposta, si salvino insieme anche le intestazioni di risposta ricevute, quindi le copie salvate dalla CDN prima della correzione continuano a uscire come [Content-Type: text/html] senza charset. Quando scadono e vengono riprese dal server di origine, oppure quando svuoti dalla cache della CDN solo l'indirizzo di quell'articolo, passano alla nuova intestazione con charset.

Lo standard URL stabilisce che i caratteri non ASCII nel percorso vengano convertiti in codifica percentuale UTF-8 prima della richiesta, quindi un lettore che ha letto correttamente il documento come UTF-8 richiede il nome di file originale. I nomi di file con sole lettere latine e cifre restano uguali anche se il documento viene letto per errore come windows-1252, quindi i loro indirizzi non si rompono nemmeno nelle pagine in cui mancano sia il charset nell'intestazione di risposta sia il meta charset.

ℹ️ Informativa sull'affiliazione
I contenuti di questo sito includono link di affiliazione. Se un visitatore acquista un prodotto o un servizio tramite uno di questi link, il sito riceve una commissione dal venditore. In questo processo l'importo pagato dall'acquirente (durante le promozioni l'importo scende↓) non aumenta. Prezzi, sconti e disponibilità indicati si riferiscono al momento della stesura e possono differire da quelli reali: prima di acquistare verifica sempre presso il venditore. I prodotti vengono scelti e valutati secondo criteri propri, e le commissioni non influiscono sull'ordine di presentazione né sul contenuto delle valutazioni.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Dimostra di essere umano: 6   +   1   =