meta charset utf-8: ошибки 404 из-за корейских адресов изображений, сломанных краулером AdSense, и переполненный php-fpm

Тестовая средаСемейство RHELAlmaLinux 9.8Связка Nginx и Apachenginx сразу отдаёт статический HTML плагина кешированияПроверено 2026-11-04
Часть адресов, путей и чисел — примерные значения

Если читающая сторона не находит объявление meta charset utf-8 в начале HTML, а в заголовке ответа тоже нет charset, ей приходится угадывать кодировку символов документа или читать его со значением по умолчанию. На сервере WordPress со связкой Nginx и Apache краулер AdSense массово отправил запросы к сломанным корейским (не-ASCII) адресам изображений, и обработка 404 заняла все воркеры PHP. После разбора логов в заголовок ответа кешированных страниц добавлен charset, а nginx теперь сразу отвечает 404 на несуществующие статические файлы, поэтому те же запросы больше не забивают воркеры.

1️⃣ meta charset utf-8: почему он должен быть в первых 1024 байтах

Сервер может сообщить кодировку символов документа в двух местах: в charset после заголовка ответа [Content-Type] и в строке meta charset внутри HTML.

Стандарт HTML требует, чтобы объявление кодировки находилось только в первых 1024 байтах, и советует читающей стороне заранее читать только эти первые 1024 байта, чтобы его найти. Поэтому даже внутри head meta charset, сдвинутый за 1024 байта, некоторые читающие стороны не находят.

Чтобы выбрать кодировку, читающая сторона сначала смотрит на BOM (метку порядка байтов), затем на charset в заголовке ответа, а потом на meta charset, найденный при предварительном чтении первых 1024 байтов. Если нет ни одного из трёх, она угадывает по виду байтов или берёт заданное значение по умолчанию, а в таблице значений по умолчанию, которую предлагает стандарт HTML, для большинства locale это windows-1252.

Если прочитать документ в UTF-8 как windows-1252, один символ из нескольких байтов, например корейская буква, превращается в несколько других символов.

🧭 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️⃣ Корейские (не-ASCII) адреса изображений, которые сломал краулер AdSense Mediapartners-Google

Проверка кешированной страницы до исправления по критерию первых 1024 байтов показала: в заголовке ответа Content-Type не было charset, а строка meta charset стояла на 15 199-м байте. То есть в кешированной странице, которую получил краулер AdSense Mediapartners-Google, кодировку нельзя было найти ни в одном из двух мест.

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

В этой связке, если добавить к адресу строку запроса, кешированная страница пропускается и приходит ответ, который PHP сформировал напрямую, и в этом ответе charset был.

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

С какого байта в файле кеша начинается meta charset, можно проверить с точностью до байта через [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

В тегах img того же файла кеша многоязычные имена файлов в UTF-8 (в данном случае корейские) стояли как есть, без процентного кодирования. Если прочитать этот HTML как windows-1252, имя вроде [대시보드-화면-예시.png] превращается в символы вроде [대시보드-화면-예시.png], а если эти символы снова закодировать в процентах как UTF-8 и запросить, сервер ищет файл, которого нет.

Если выбрать из журнала доступа ошибки 404 изображений, которые получил этот краулер, адреса со сломанными именами идут один за другим.

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

Адреса в журнале побайтно совпадали со значением, которое получается, если прочитать UTF-8-байты исходного имени файла как символы windows-1252 и снова закодировать в UTF-8. В журнале за тот день запросов к так же сломанным адресам изображений было 202 от этого краулера и 6 от одного скрипта (python-requests), а от посетителей — 0.

Если пропустить сломанное имя [대시보드-화면-예시.png] через инструмент Конвертер UTF-8, оно определяется как UTF-8-байты, прочитанные как windows-1252, и появляется исходное имя [대시보드-화면-예시.png].

3️⃣ Как ошибки 404 несуществующих изображений заполнили php-fpm

Поскольку изображений по сломанным адресам на сервере нет, nginx по последнему пункту try_files в своём правиле location для изображений передал запросы в Apache, а WordPress сформировал страницу 404 через PHP. Среди 587 ошибок 404, записанных днём в тот же день, каждая страница 404 без кеша занимала в медиане 1,4 секунды.

🌐 Mediapartners-GoogleЗапрос изображения со сломанным именем
location изображений в nginxtry_files проверяет файл на диске
Файл естьnginx сразу отвечает 200
Файла нетПередача в бэкенд, последний пункт try_files
Apache → WordPressСтраница 404 через PHP (медиана 1,4 секунды)
Воркеры php-fpmОдин воркер на запрос
Предел пула (pm.max_children)При заполнении следующие запросы ждут (499, 504)

Запросы изображений со сломанными именами накопились до чуть более 200 за 3 минуты начиная с 20:40, и 133 из них закончились ошибкой 404. Каждому запросу выделяется один воркер php-fpm, и поскольку запросы продолжали поступать, пока каждая ошибка 404 занимала от 4 до 6 секунд, пул упёрся в предел в 20:40:55.

Момент, когда пул упирается в предел, остаётся в журнале ошибок php-fpm: Предупреждение 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

Запросы, пришедшие после заполнения пула, ждали свободного воркера. Дольше всех ждавшая ошибка 404 завершилась через 58 секунд, запросы, которые краулер оборвал после 60 секунд ожидания, записаны как 499, а один запрос, не получивший ответа от бэкенда, — как 504.

Предзагрузку кеша, запланированные задачи (Action Scheduler), работавшие в то же время, и wp-cron, запускаемый каждую минуту, были проверены и исключены из причин. Предзагрузка весь день шла с одной и той же низкой скоростью, запланированные задачи в это время обрабатывали только задания предзагрузки, а wp-cron запускается через PHP из командной строки и не использует воркеры php-fpm.

4️⃣ nginx charset utf-8 и сразу 404 для несуществующих статических файлов

В location nginx, которая отдаёт кешированные страницы, добавлена одна строка [charset utf-8;], чтобы в заголовке ответа был charset, а для изображений и статических файлов в папке загрузок nginx теперь сразу возвращает 404, если их нет на диске, вместо передачи в WordPress.

# Блок http: значение Cache-Control задаётся только для ответов 404
map $status $up404_cc { 404 'no-store'; default ''; }

# location, которая сразу отдаёт кешированный HTML
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# Папка загрузок: несуществующие изображения сразу получают 404 без передачи в бэкенд
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
    try_files $uri =404;
    add_header Cache-Control $up404_cc always;
}

map размещается в блоке http, а поскольку nginx не отправляет заголовок с пустой строкой в значении, ответы 200 не получают no-store. location с регулярными выражениями применяются сверху вниз, и срабатывает первая подходящая, поэтому она стоит перед существующей location изображений, а для статических файлов вроде css, js и шрифтов добавлена ещё одна location того же вида.

После исправления повторный запрос того же адреса показал, что в заголовке ответа кешированной страницы появился 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)'
# До исправления: 404, сформированная WordPress
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# После исправления: 404, которую nginx вернул сразу
HTTP/2 404
content-type: text/html
cache-control: no-store

Ошибка 404 для несуществующего изображения сменилась со страницы WordPress, которая формировала 319 058 байт за 1,01 секунды, на стандартную страницу nginx, которая возвращается за 0,015 секунды и весит 555 байт. Поскольку [Cache-Control: no-store] добавляется только к 404, CDN не сохраняет ошибки 404 несуществующих изображений. К ответам, которые PHP уже отправил с charset, nginx не добавляет charset повторно, поэтому в заголовке ответа он появился только один раз.

Проверенный запросДо исправленияПосле исправления
Несуществующее изображение319 058 B, 1,01 секунды (404 WordPress)555 B, 0,015 секунды (404 nginx)
Изображение со сломанным именем1,05 секунды (404 WordPress)0,014 секунды (404 nginx)
Заголовок ответа кешированного HTMLtext/htmltext/html; charset=utf-8
Заголовок ответа, сформированный PHP напрямуюtext/html; charset=UTF-8Так же (charset один раз)
Существующее изображение200По-прежнему 200

Такая обработка 404 не подходит сайтам с плагинами изображений, которые создают миниатюры или WebP через PHP уже после поступления запроса. Поскольку nginx первым возвращает 404, изображение так и не создаётся. Если ваша конфигурация заранее создаёт файлы WebP при загрузке, её можно применить как есть.

5️⃣ Почему meta charset сдвинулся назад: WP Rocket Delay JS

Даже после добавления charset в заголовок ответа meta charset в кешированном HTML по-прежнему стоит на 15 199-м байте. На странице, которую PHP формирует напрямую, та же строка стояла на 135-м байте.

Если в WP Rocket включён Delay JS, при создании файла кеша сразу после head вставляются скрипты отложенного выполнения вроде lazyload, а затем отдельный шаг возвращает meta charset в самое начало head. Этот шаг применяет регулярное выражение ко всему документу, поэтому на большой записи размером около 1,2 МБ он превышает предел backtrack в PHP и завершается неудачей.

Когда регулярное выражение не срабатывает, WP Rocket кеширует HTML как есть — со вставленными скриптами, но без перенесённого meta charset, — и в результате meta charset оказался за пределами первых 1024 байтов. В отдельном воспроизведении с теми же настройками PHP перенос не удался при пределе backtrack по умолчанию 1 000 000 и при 5 000 000 и удался начиная с 10 000 000.

На этот раз задача решена добавлением charset в заголовок ответа, а положение meta charset не исправлялось. Повышение предела backtrack возвращает исходное поведение WP Rocket, но увеличивает в 10 раз предел для всех регулярных выражений в том же PHP, поэтому этот путь не рекомендуется.

🔢 FAQ & рекомендуемые материалы

В документации Google по краулерам сказано, что настройки сканирования для Mediapartners-Google влияют на рекламные продукты, такие как AdSense и Ad Manager. Этот краулер не следует общему правилу robots.txt [User-agent: *], поэтому, чтобы его заблокировать, нужно указать его имя отдельно. Если проблема в ошибках 404 по сломанным адресам, лучше, чтобы веб-сервер сразу отвечал 404 на несуществующие файлы: так сканирование рекламы не затрагивается.

Стандарт HTTP-кеширования требует при сохранении ответа сохранять вместе с ним полученные заголовки ответа, поэтому копии, которые CDN сохранила до исправления, по-прежнему уходят как [Content-Type: text/html] без charset. Когда срок хранения истечёт и они будут заново получены с исходного сервера или когда вы очистите в кеше CDN только адрес этой записи, они перейдут на новый заголовок с charset.

Стандарт URL требует перед запросом переводить символы вне ASCII в пути адреса в процентное кодирование UTF-8, поэтому сторона, правильно прочитавшая документ как UTF-8, запрашивает исходное имя файла. Имена файлов только из латинских букв и цифр остаются теми же, даже если документ ошибочно прочитан как windows-1252, поэтому их адреса не ломаются даже на страницах, где нет ни charset в заголовке ответа, ни meta charset.

🔗 Рекомендуемые материалы
Другие замеры краулеров ИИ-краулеры: замер визитов GPTBot и ClaudeBot и блокировка через robots.txt и nginx ИИ-краулеры — это боты, с помощью которых ИИ-компании собирают веб-страницы для обучения моделей, ИИ-поиска и ответов на вопросы пользователей. UA — это текст, который запрашивающая сторона сама пишет и отправляет, поэтому кто угодно может отправлять запросы под именем GPTBot или ClaudeBot. Читать → Путь запроса ИИ-краулера: robots.txt останавливает только ботов, читающих правила, блокировка по UA в nginx смотрит только на имя, а самозванцы, взявшие имя разрешённого бота, отсекаются сверкой с опубликованными диапазонами IP

ℹ️ О партнёрских ссылках
Материалы этого сайта содержат партнёрские ссылки. Если посетитель покупает товар или услугу по такой ссылке, сайт получает комиссию от продавца. При этом сумма, которую платит покупатель (при акционных скидках сумма снижается ↓), не увеличивается. Указанные цены, скидки и наличие актуальны на момент написания и могут отличаться, поэтому перед покупкой уточняйте их у продавца. Товары выбираются и оцениваются по нашим собственным критериям, а комиссия не влияет ни на порядок, ни на содержание обзоров.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Докажите, что вы человек: 10   +   6   =