meta charset utf-8: 404 por endereços de imagens em coreano quebrados pelo rastreador do AdSense e php-fpm lotado
Se o leitor não encontrar a declaração meta charset utf-8 no início do HTML e o cabeçalho de resposta também não tiver charset, ele precisa adivinhar a codificação de caracteres do documento ou ler com um valor padrão. Em um servidor WordPress com Nginx e Apache, o rastreador do AdSense enviou uma rajada de requisições para endereços de imagens em coreano (não ASCII) quebrados, e o tratamento dos 404 ocupou todos os workers do PHP. Depois de rastrear isso nos logs, adicionei charset ao cabeçalho de resposta das páginas em cache e fiz o nginx responder diretamente com 404 aos arquivos estáticos inexistentes, para que as mesmas requisições não voltem a lotar os workers.
1️⃣ meta charset utf-8: por que precisa estar nos primeiros 1.024 bytes
O servidor pode informar a codificação de caracteres do documento em dois lugares: o charset que vem depois do cabeçalho de resposta [Content-Type] e a linha meta charset escrita dentro do HTML.
O padrão HTML determina que a declaração de codificação apareça somente nos primeiros 1.024 bytes, e recomenda ao leitor que faça uma leitura prévia apenas desses primeiros 1.024 bytes para encontrá-la. Por isso, mesmo dentro de head, um meta charset empurrado além de 1.024 bytes pode passar despercebido por alguns leitores.
Para decidir a codificação, o leitor olha primeiro o BOM (marca de ordem de bytes), depois o charset do cabeçalho de resposta e, em seguida, o meta charset encontrado na leitura prévia dos primeiros 1.024 bytes. Se nenhum dos três existir, ele adivinha pelo formato dos bytes ou usa um padrão predefinido, e na tabela de padrões sugerida pelo padrão HTML, a maioria dos locale fica com windows-1252.
Quando um documento UTF-8 é lido como windows-1252, um único caractere de vários bytes, como uma letra coreana, vira vários caracteres diferentes.
2️⃣ Endereços de imagens em coreano (não ASCII) quebrados pelo rastreador do AdSense Mediapartners-Google
Ao verificar a página em cache de antes da correção com o critério dos primeiros 1.024 bytes, o cabeçalho de resposta Content-Type não tinha charset e a linha meta charset estava no byte 15.199. Ou seja, a página em cache que o rastreador do AdSense Mediapartners-Google recebeu não tinha uma codificação que pudesse ser encontrada em nenhum dos dois lugares.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlNesta configuração, se você adicionar uma string de consulta ao endereço, a página em cache é ignorada e chega uma resposta gerada diretamente pelo PHP, e essa resposta trazia charset.
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8Com [grep -b] você vê, byte a byte, em que posição do arquivo de cache o meta charset começa.
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charsetAs tags img do mesmo arquivo de cache traziam nomes de arquivo multilíngues em UTF-8 (neste caso, em coreano) do jeito que estavam, sem codificação percentual. Se esse HTML for lido como windows-1252, um nome como [대시보드-화면-예시.png] vira caracteres como [대시보드-화면-예시.png], e se esses caracteres forem codificados de novo em percentual como UTF-8 e solicitados, o servidor procura um arquivo que não existe.
Ao extrair do log de acesso os 404 de imagens que esse rastreador recebeu, aparecem endereços com nomes quebrados um atrás do outro.
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 404Os endereços que ficaram no log eram idênticos, byte a byte, ao valor obtido lendo os bytes UTF-8 do nome original como caracteres windows-1252 e codificando de novo em UTF-8. No log daquele dia, as requisições para endereços de imagens quebrados desse mesmo jeito foram 202 desse rastreador e 6 de um único script (python-requests), e 0 de visitantes.
Se você passar o nome quebrado [대시보드-화면-예시.png] pela ferramenta Conversor UTF-8, ele é identificado como bytes UTF-8 lidos como windows-1252, e o nome original [대시보드-화면-예시.png] aparece.
3️⃣ Como os 404 de imagens inexistentes lotaram o php-fpm
Como as imagens dos endereços quebrados não existem no servidor, o nginx repassou as requisições ao Apache seguindo o último item do try_files na sua regra location de imagens, e o WordPress gerou a página 404 com PHP. Olhando os 587 registros de 404 durante o dia nessa mesma data, cada página 404 sem cache levou uma mediana de 1,4 segundo.
As requisições de imagens com nomes quebrados se acumularam em pouco mais de 200 nos 3 minutos a partir de 20:40, e 133 delas terminaram em 404. Cada requisição recebe um worker do php-fpm, e como as requisições continuaram chegando enquanto cada 404 levava de 4 a 6 segundos, o pool atingiu o limite às 20:40:55.
O momento em que o pool atinge o limite fica registrado no log de erros do php-fpm: Aviso de 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 itAs requisições que chegaram depois que o pool encheu esperaram por um worker livre. O 404 que mais esperou terminou em 58 segundos, as requisições que o rastreador cortou depois de esperar 60 segundos foram registradas como 499, e uma requisição que não recebeu resposta do backend, como 504.
O pré-carregamento do cache, as tarefas agendadas (Action Scheduler) que rodavam no mesmo horário e o wp-cron, executado a cada minuto, foram verificados e descartados como causa. O pré-carregamento rodou o dia todo no mesmo ritmo baixo, as tarefas agendadas só processavam trabalhos de pré-carregamento naquele horário, e o wp-cron roda com PHP de linha de comando e não usa workers do php-fpm.
4️⃣ nginx charset utf-8 e 404 direto para arquivos estáticos inexistentes
Adicionei a linha [charset utf-8;] à location do nginx que entrega as páginas em cache para que o cabeçalho de resposta traga charset, e para imagens e arquivos estáticos da pasta de uploads, se não estiverem no disco, o nginx agora devolve 404 diretamente, sem repassar ao WordPress.
# Bloco http: define um valor de Cache-Control só para respostas 404
map $status $up404_cc { 404 'no-store'; default ''; }
# location que entrega diretamente o HTML em cache
location / {
try_files $cache_file @backend;
charset utf-8;
}
# Pasta de uploads: imagens inexistentes recebem 404 direto, sem ir ao backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}O map fica no bloco http, e como o nginx não envia um cabeçalho cujo valor é uma string vazia, as respostas 200 não recebem no-store. As locations com expressão regular são aplicadas de cima para baixo e vale a primeira que corresponder, então coloquei esta antes da location de imagens existente e adicionei mais uma location do mesmo tipo para arquivos estáticos como css, js e fontes.
Ao solicitar de novo o mesmo endereço depois da correção, o cabeçalho de resposta da página em cache passou a trazer 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)'# Antes da correção: um 404 gerado pelo WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Depois da correção: um 404 devolvido diretamente pelo nginx
HTTP/2 404
content-type: text/html
cache-control: no-storeO 404 de uma imagem inexistente passou de uma página do WordPress que gerava 319.058 bytes em 1,01 segundo para a página padrão do nginx, devolvida em 0,015 segundo com 555 bytes. Ao adicionar [Cache-Control: no-store] apenas aos 404, a CDN não guarda os 404 de imagens inexistentes. O nginx não adiciona charset de novo às respostas que o PHP já enviou com charset, por isso ele apareceu só uma vez no cabeçalho de resposta.
| Requisição verificada | Antes da correção | Depois da correção |
|---|---|---|
| Imagem inexistente | 319.058 B, 1,01 segundo (404 do WordPress) | 555 B, 0,015 segundo (404 do nginx) |
| Imagem com nome quebrado | 1,05 segundo (404 do WordPress) | 0,014 segundo (404 do nginx) |
| Cabeçalho de resposta do HTML em cache | text/html | text/html; charset=utf-8 |
| Cabeçalho de resposta gerado diretamente pelo PHP | text/html; charset=UTF-8 | Igual (charset uma vez) |
| Imagem existente | 200 | Continua 200 |
Esse tratamento de 404 não serve para sites com plugins de imagem que geram miniaturas ou WebP com PHP depois que a requisição chega. Como o nginx devolve o 404 antes, a imagem nunca é gerada. Se a sua configuração cria os arquivos WebP com antecedência no upload, você pode aplicar do jeito que está.
5️⃣ Por que o meta charset foi empurrado para trás: Delay JS do WP Rocket
Mesmo depois de adicionar charset ao cabeçalho de resposta, o meta charset do HTML em cache continua no byte 15.199. Na página gerada diretamente pelo PHP, essa mesma linha estava no byte 135.
Com o Delay JS do WP Rocket ativado, ao criar o arquivo de cache são inseridos scripts de execução adiada, como o lazyload, logo depois de head, e em seguida uma etapa move o meta charset de volta para o início de head. Essa etapa aplica uma expressão regular ao documento inteiro, então em um post grande de cerca de 1,2 MB ela ultrapassa o limite de backtrack do PHP e falha.
Quando a expressão regular falha, o WP Rocket guarda em cache o HTML do jeito que está, com os scripts inseridos mas sem mover o meta charset, e com isso o meta charset ficou fora dos primeiros 1.024 bytes. Em uma reprodução separada com as mesmas configurações de PHP, a movimentação falhou com o limite de backtrack no padrão de 1.000.000 e em 5.000.000, e funcionou a partir de 10.000.000.
Desta vez resolvi adicionando charset ao cabeçalho de resposta e não corrigi a posição do meta charset. Aumentar o limite de backtrack traz de volta o comportamento original do WP Rocket, mas multiplica por dez o limite de todas as expressões regulares que rodam no mesmo PHP, por isso não recomendo.
🔢 FAQ & conteúdo recomendado
HTTP 404
Erro HTTP 404 (erro do cliente): causas e solução
A página de erro HTTP 404, um dos códigos de status HTTP, aparece quando uma página não é encontrada.
AdSense
Configuração do ads.txt no WordPress: como resolver o aviso do AdSense
ads.txt é um arquivo de texto colocado na raiz do site que lista publicamente quais vendedores autorizados podem vender o espaço publicitário do domínio.
Cache do WP Rocket
WP Rocket e URLs em coreano: por que o nginx não encontra o arquivo de cache e a solução com njs
Em posts com slug em inglês, o nginx entrega o arquivo de cache diretamente, mas em posts com slug em coreano, mesmo que o arquivo de cache exista, ele só é entregue depois que o PHP roda uma vez.
ℹ️ Aviso de afiliados
O conteúdo deste site contém links de afiliados. Quando um visitante compra um produto ou serviço por meio de um desses links, o site recebe uma comissão do vendedor. O valor que o comprador paga (em promoções, ele diminui ↓) não aumenta por causa disso. Preços, descontos e informações de estoque valem para o momento em que o texto foi escrito e podem ter mudado, por isso confirme com o vendedor antes de comprar. Os produtos são escolhidos e avaliados por critérios próprios, e o recebimento de comissão não influencia a ordem de apresentação nem o conteúdo das avaliações.