meta charset utf-8: 404 por endereços de imagens em coreano quebrados pelo rastreador do AdSense e php-fpm lotado

Ambiente de testeFamília RHELAlmaLinux 9.8Configuração com Nginx e ApacheO nginx entrega diretamente o HTML estático do plugin de cacheVerificado em 2026-11-04
Alguns endereços, caminhos e números são valores de exemplo

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.

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

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

Com [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 -1
15199:<meta charset

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

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

🌐 Mediapartners-GoogleRequisição de imagem com nome quebrado
location de imagens do nginxtry_files verifica o arquivo no disco
Arquivo existenginx responde 200 diretamente
Arquivo não existeRepassa ao backend, último item do try_files
Apache → WordPressPágina 404 renderizada com PHP (mediana de 1,4 segundo)
Workers do php-fpmUm worker por requisição
Limite do pool (pm.max_children)Cheio: as próximas requisições esperam (499, 504)

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 it

As 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-8
curl -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-store

O 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 verificadaAntes da correçãoDepois da correção
Imagem inexistente319.058 B, 1,01 segundo (404 do WordPress)555 B, 0,015 segundo (404 do nginx)
Imagem com nome quebrado1,05 segundo (404 do WordPress)0,014 segundo (404 do nginx)
Cabeçalho de resposta do HTML em cachetext/htmltext/html; charset=utf-8
Cabeçalho de resposta gerado diretamente pelo PHPtext/html; charset=UTF-8Igual (charset uma vez)
Imagem existente200Continua 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

A documentação de rastreadores do Google diz que as regras de rastreamento voltadas ao Mediapartners-Google afetam produtos de anúncios como o AdSense e o Ad Manager. Esse rastreador não segue a regra geral do robots.txt [User-agent: *], então para bloqueá-lo é preciso citá-lo pelo nome separadamente. Se o problema são os 404 de endereços quebrados, deixar o servidor web responder diretamente com 404 aos arquivos inexistentes não mexe no rastreamento de anúncios.

O padrão de cache HTTP determina que, ao guardar uma resposta, os cabeçalhos de resposta recebidos sejam guardados junto, então as cópias que a CDN guardou antes da correção continuam saindo como [Content-Type: text/html] sem charset. Quando expiram e são buscadas de novo no servidor de origem, ou quando você limpa do cache da CDN só o endereço daquele post, elas passam para o novo cabeçalho com charset.

O padrão URL determina que caracteres não ASCII do caminho sejam convertidos em codificação percentual UTF-8 antes da requisição, então um leitor que leu o documento corretamente como UTF-8 solicita o nome de arquivo original. Nomes de arquivo só com letras latinas e números continuam iguais mesmo que o documento seja lido por engano como windows-1252, então seus endereços não quebram nem em páginas em que faltam tanto o charset do cabeçalho de resposta quanto o meta charset.

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

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Demonstre sua humanidade: 8   +   9   =