meta charset utf-8: 404 por direcciones de imágenes en coreano que rompió el rastreador de AdSense y php-fpm lleno

Entorno de pruebaFamilia RHELAlmaLinux 9.8Configuración con Nginx y Apachenginx sirve directamente el HTML estático del plugin de cachéVerificado el 2026-11-04
Algunas direcciones, rutas y cifras son valores de ejemplo

Si el lector no encuentra la declaración meta charset utf-8 al principio del HTML y el encabezado de respuesta tampoco trae charset, tiene que adivinar la codificación de caracteres del documento o leerlo con un valor predeterminado. En un servidor WordPress con Nginx y Apache, el rastreador de AdSense envió una ráfaga de solicitudes a direcciones de imágenes en coreano (no ASCII) rotas, y el manejo de los 404 llenó todos los workers de PHP. Tras seguirlo en los logs, añadí charset al encabezado de respuesta de las páginas en caché e hice que nginx respondiera directamente con un 404 a los archivos estáticos inexistentes, para que las mismas solicitudes no vuelvan a llenar los workers.

1️⃣ meta charset utf-8: por qué debe estar en los primeros 1.024 bytes

El servidor puede indicar la codificación de caracteres del documento en dos lugares: el charset que va detrás del encabezado de respuesta [Content-Type] y la línea meta charset escrita dentro del HTML.

El estándar HTML exige que la declaración de codificación aparezca solo dentro de los primeros 1.024 bytes, y recomienda al lector que busque leyendo por adelantado solo esos primeros 1.024 bytes. Por eso, aunque esté dentro de head, un meta charset desplazado más allá de 1.024 bytes puede pasar desapercibido para algunos lectores.

Para decidir la codificación, el lector mira primero el BOM (marca de orden de bytes), después el charset del encabezado de respuesta y luego el meta charset encontrado al leer por adelantado los primeros 1.024 bytes. Si no hay ninguno de los tres, adivina por la forma de los bytes o usa un valor predeterminado, y en la tabla de valores predeterminados que propone el estándar HTML, la mayoría de los locale tienen windows-1252.

Si se lee un documento UTF-8 como windows-1252, un solo carácter de varios bytes, como una letra coreana, se convierte en varios caracteres distintos.

🧭 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️⃣ Direcciones de imágenes en coreano (no ASCII) que rompió el rastreador de AdSense Mediapartners-Google

Al revisar la página en caché de antes del arreglo con el criterio de los primeros 1.024 bytes, el encabezado de respuesta Content-Type no tenía charset y la línea meta charset estaba en el byte 15.199. Así que la página en caché que recibió el rastreador de AdSense Mediapartners-Google no tenía una codificación que pudiera encontrarse en ninguno de los dos lugares.

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

En esta configuración, si añades una cadena de consulta a la dirección, se salta la página en caché y llega una respuesta generada directamente por PHP, y esa respuesta sí llevaba charset.

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

Con [grep -b] puedes ver, byte a byte, en qué posición del archivo de caché empieza el 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

Las etiquetas img del mismo archivo de caché contenían nombres de archivo multilingües en UTF-8 (en este caso, en coreano) tal cual, sin codificación porcentual. Si este HTML se lee como windows-1252, un nombre como [대시보드-화면-예시.png] se convierte en caracteres como [대시보드-화면-예시.png], y si esos caracteres se vuelven a codificar en porcentaje como UTF-8 y se solicitan, el servidor busca un archivo que no existe.

Si extraes del log de acceso los 404 de imágenes que recibió este rastreador, aparecen una tras otra direcciones con nombres rotos.

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

Las direcciones que quedaron en el log coincidían byte a byte con los bytes UTF-8 del nombre original leídos como caracteres windows-1252 y codificados de nuevo en UTF-8. Ese día, el log tenía 202 solicitudes a direcciones de imágenes rotas de esta misma forma desde este rastreador y 6 desde un script (python-requests), y 0 desde visitantes.

Si pasas el nombre roto [대시보드-화면-예시.png] por la herramienta Conversor UTF-8, se detecta como bytes UTF-8 leídos como windows-1252 y aparece el nombre original [대시보드-화면-예시.png].

3️⃣ Cómo los 404 de imágenes inexistentes llenaron php-fpm

Como las imágenes de las direcciones rotas no existen en el servidor, nginx pasó las solicitudes a Apache siguiendo el último elemento de try_files en su regla de ubicación de imágenes, y WordPress generó la página 404 con PHP. Mirando los 587 404 registrados durante el día en esa misma fecha, cada página 404 sin caché tardó una mediana de 1,4 segundos.

🌐 Mediapartners-GoogleSolicitud de imagen con nombre roto
Ubicación de imágenes de nginx (location)try_files comprueba el archivo en el disco
El archivo existenginx responde 200 directamente
El archivo no existePasa al backend, el último elemento de try_files
Apache → WordPressPágina 404 generada con PHP (mediana de 1,4 segundos)
Workers de php-fpmUn worker por solicitud
Límite del pool (pm.max_children)Lleno: las siguientes solicitudes esperan (499, 504)

Las solicitudes de imágenes con nombres rotos se acumularon hasta algo más de 200 en los 3 minutos desde las 20:40, y 133 de ellas terminaron en 404. Cada solicitud recibe un worker de php-fpm, y como las solicitudes seguían llegando mientras cada 404 tardaba de 4 a 6 segundos, el pool llegó a su límite a las 20:40:55.

El momento en que el pool llega a su límite queda registrado en el log de errores de php-fpm como 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

Las solicitudes que llegaron después de llenarse el pool esperaron a que quedara un worker libre. El 404 que más esperó terminó a los 58 segundos, las solicitudes que el rastreador cortó tras esperar 60 segundos quedaron registradas como 499, y una solicitud que no recibió respuesta del backend quedó como 504.

La precarga de la caché, las tareas programadas (Action Scheduler) que corrían a la misma hora y wp-cron, que se ejecuta cada minuto, se revisaron y se descartaron como causa. La precarga funcionó todo el día al mismo ritmo bajo, las tareas programadas solo procesaban trabajos de precarga a esa hora, y wp-cron se ejecuta con PHP de línea de comandos, así que no usa workers de php-fpm.

4️⃣ nginx charset utf-8 y 404 directo para archivos estáticos inexistentes

Añadí la línea [charset utf-8;] a la ubicación de nginx que sirve las páginas en caché para que el encabezado de respuesta lleve charset, y para las imágenes y archivos estáticos de la carpeta de subidas, si no están en el disco, nginx devuelve un 404 directamente en vez de pasarlos a WordPress.

# Bloque http: fija un valor de Cache-Control solo para las respuestas 404
map $status $up404_cc { 404 'no-store'; default ''; }

# Ubicación que sirve directamente el HTML en caché
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# Carpeta de subidas: las imágenes inexistentes reciben un 404 directo sin pasar al backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
    try_files $uri =404;
    add_header Cache-Control $up404_cc always;
}

El map va en el bloque http, y como nginx no envía un encabezado cuyo valor es una cadena vacía, las respuestas 200 no reciben no-store. Las ubicaciones con expresiones regulares se aplican de arriba abajo y gana la primera que coincide, así que la coloqué antes de la ubicación de imágenes existente y añadí otra ubicación del mismo tipo para archivos estáticos como css, js y fuentes.

Al volver a solicitar la misma dirección tras el arreglo, el encabezado de respuesta de la página en caché ya llevaba 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 del arreglo: un 404 generado por WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Después del arreglo: un 404 devuelto directamente por nginx
HTTP/2 404
content-type: text/html
cache-control: no-store

El 404 de una imagen inexistente pasó de una página de WordPress que tardaba 1,01 segundos en generar 319.058 bytes a la página predeterminada de nginx, devuelta en 0,015 segundos con 555 bytes. Al añadir [Cache-Control: no-store] solo a los 404, la CDN no guarda los 404 de imágenes inexistentes. nginx no vuelve a añadir charset a las respuestas que PHP ya envió con él, así que apareció una sola vez en el encabezado de respuesta.

Solicitud comprobadaAntes de la correcciónDespués del arreglo
Imagen inexistente319.058 B, 1,01 segundos (404 de WordPress)555 B, 0,015 segundos (404 de nginx)
Imagen con nombre roto1,05 segundos (404 de WordPress)0,014 segundos (404 de nginx)
Encabezado de respuesta del HTML en cachétext/htmltext/html; charset=utf-8
Encabezado de respuesta generado directamente por PHPtext/html; charset=UTF-8Igual (charset una vez)
Imagen existente200Sigue en 200

Este manejo de 404 no sirve para sitios con plugins de imágenes que generan miniaturas o WebP con PHP después de que llega la solicitud. Como nginx devuelve primero el 404, la imagen nunca se genera. Si tu configuración crea los archivos WebP de antemano al subirlos, puedes aplicarlo tal cual.

5️⃣ Por qué meta charset quedó desplazado: Delay JS de WP Rocket

Incluso después de añadir charset al encabezado de respuesta, el meta charset del HTML en caché sigue en el byte 15.199. En la página generada directamente por PHP, esa misma línea estaba en el byte 135.

Con Delay JS de WP Rocket activado, al crear el archivo de caché se insertan scripts de ejecución diferida como lazyload justo después de head, y luego un paso vuelve a mover meta charset al principio de head. Ese paso aplica una expresión regular a todo el documento, así que en una entrada grande de unos 1,2 MB supera el límite de backtrack de PHP y falla.

Cuando la expresión regular falla, WP Rocket guarda en caché el HTML tal cual, con los scripts insertados pero sin mover meta charset, y por eso meta charset quedó fuera de los primeros 1.024 bytes. En una reproducción aparte con la misma configuración de PHP, el movimiento falló con el límite de backtrack en el valor predeterminado de 1.000.000 y en 5.000.000, y funcionó a partir de 10.000.000.

Esta vez lo resolví añadiendo charset al encabezado de respuesta y no corregí la posición de meta charset. Subir el límite de backtrack recupera el comportamiento original de WP Rocket, pero multiplica por diez el límite de todas las expresiones regulares que corren en el mismo PHP, así que no lo recomiendo.

🔢 FAQ & contenido recomendado

La documentación de rastreadores de Google indica que las reglas de rastreo dirigidas a Mediapartners-Google afectan a productos publicitarios como AdSense y Ad Manager. Este rastreador no sigue la regla general de robots.txt [User-agent: *], así que para bloquearlo hay que nombrarlo por separado. Si el problema son los 404 de direcciones rotas, que el servidor web responda directamente con un 404 a los archivos inexistentes no toca el rastreo de anuncios.

El estándar de caché HTTP exige guardar los encabezados de respuesta recibidos junto con la respuesta, así que las copias que la CDN guardó antes del arreglo siguen saliendo como [Content-Type: text/html] sin charset. Cuando caducan y se piden de nuevo al servidor de origen, o cuando purgas de la caché de la CDN solo la dirección de esa entrada, pasan al nuevo encabezado con charset.

El estándar URL exige convertir los caracteres no ASCII de la ruta a codificación porcentual UTF-8 antes de la solicitud, así que un lector que leyó bien el documento como UTF-8 solicita el nombre de archivo original. Los nombres de archivo con solo letras latinas y números quedan iguales aunque el documento se lea mal como windows-1252, de modo que sus direcciones no se rompen ni en páginas a las que les faltan tanto el charset del encabezado de respuesta como meta charset.

ℹ️ Aviso de afiliados
El contenido de este sitio incluye enlaces de afiliado. Cuando un visitante compra un producto o servicio a través de uno de ellos, el sitio recibe una comisión del vendedor. El importe que paga el comprador(baja durante las promociones ↓)no aumenta. Los precios, descuentos y existencias publicados corresponden al momento de redacción y pueden variar, así que confírmalos con el vendedor antes de comprar. Los productos se eligen y evalúan con criterios propios, y las comisiones no influyen en el orden ni en el contenido de las reseñas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Demuestra que eres humano: 5   +   10   =