Когда API возвращает HTML вместо JSON, клиент обычно показывает ошибку разбора вроде Unexpected token <, хотя настоящая причина находится на сервере. Вместо ожидаемого объекта приложение получает страницу входа, шаблон 404, debug-экран фреймворка, ответ reverse proxy или защитную страницу CDN.
Исправлять JSON-парсер вслепую не нужно. Сначала важно сохранить исходный HTTP-ответ и определить, какой компонент сформировал HTML. Только после этого можно исправить маршрутизацию, обработку исключений или конфигурацию прокси без маскировки реальной ошибки.
Сохраните исходный ответ до повторных попыток
- Полный URL с методом запроса и query-параметрами.
- HTTP-статус до автоматических redirect.
- Заголовки Content-Type, Location, Server, Via и идентификатор запроса.
- Первые несколько сотен символов тела без персональных данных и токенов.
- Время запроса, окружение, версию клиента и release backend.
- Факт повторяемости: всегда, только без авторизации или только на части endpoints.
Не публикуйте полный HTML ошибки в открытом чате. Debug-страница может содержать пути файлов, SQL, переменные окружения, cookie, ключи интеграций и фрагменты пользовательских данных.
Проверьте ответ без клиентского приложения
Браузерный интерфейс может автоматически перейти по redirect и скрыть первоначальный ответ. Повторите запрос через HTTP-клиент с отключенным автоматическим следованием перенаправлениям. Сравните запрос с авторизацией и без нее, а также добавьте явный заголовок Accept: application/json.
- Если получен 301, 302, 303, 307 или 308, изучите Location.
- Если статус 200, но тело содержит страницу ошибки, нарушен HTTP-контракт.
- Если Content-Type равен text/html, найдите компонент, который его установил.
- Если заявлен application/json, но тело начинается с HTML, проверьте посторонний вывод.
- Если ответ меняется при Accept: application/json, проблема связана с content negotiation.
- Если ошибка появляется только снаружи, сравните прямой запрос к приложению и запрос через прокси.
Определите источник HTML
Внешний вид страницы часто подсказывает источник, но надежнее использовать заголовки, request ID и журналы каждого слоя. В типовой схеме ответ может сформировать приложение, веб-сервер, ingress, балансировщик, CDN, WAF или сервис авторизации.
- Форма входа означает, что middleware перенаправил API-запрос как обычную веб-страницу.
- Брендированная 404 часто создается frontend-роутером или общим шаблоном сайта.
- 502, 503 и 504 со стандартным дизайном обычно приходят от proxy или ingress.
- Страница challenge указывает на CDN, WAF или антибот.
- Stack trace и панель отладки формирует framework в debug-режиме.
- HTML с warning перед JSON часто появляется из-за вывода PHP до формирования ответа.
Проверьте маршрут и HTTP-метод
API endpoint может существовать для POST, но не для GET, находиться под другим version prefix или требовать завершающий slash. Если неизвестный маршрут попадает в общий обработчик сайта, сервер возвращает HTML-шаблон 404 вместо структурированной ошибки.
- Сверьте method, путь, версию API и обязательный префикс.
- Проверьте правила rewrite до передачи запроса приложению.
- Разделите fallback для SPA и namespace API.
- Не направляйте неизвестные /api/ маршруты на index.html frontend.
- Возвращайте 404 в JSON для несуществующего API endpoint.
- Проверьте, не изменяет ли proxy путь при передаче upstream.
Особенно часто проблема возникает после добавления SPA fallback. Правило, которое отдает index.html для любого неизвестного пути, должно исключать API, статические файлы и служебные endpoints.
Уберите redirect на HTML-страницу входа
Web middleware обычно отправляет неавторизованного пользователя на форму входа. Для API это неудобно: клиент ожидает 401 или 403 с JSON-телом, а получает цепочку redirect и итоговую HTML-страницу со статусом 200.
- Разделите web- и API-middleware.
- Для отсутствующей или недействительной авторизации возвращайте 401.
- Для недостаточных прав возвращайте 403.
- Не используйте redirect в ответе машинному клиенту.
- Учитывайте Accept, но не полагайтесь только на него для определения API.
- Проверьте срок действия токена, cookie и CORS до изменения кода.
JSON-ошибка должна сообщать понятный машинный код и безопасное описание. Не добавляйте в ответ секретный токен, внутренний stack trace или сведения о существовании чужих объектов.
Настройте единый обработчик исключений
Необработанное исключение часто превращается в HTML по умолчанию. Для API нужен централизованный обработчик, который сопоставляет тип ошибки с HTTP-статусом и стабильной JSON-структурой. Это касается ошибок валидации, авторизации, отсутствующих ресурсов, конфликтов и внутренних сбоев.
- Одинаковая структура содержит код ошибки, сообщение и request ID.
- Ошибки валидации возвращают список полей в предсказуемом формате.
- Внутренние исключения журналируются полностью, но наружу отдается безопасное сообщение.
- Content-Type устанавливается до отправки тела.
- Status code соответствует типу ошибки, а не всегда равен 200.
- Формат остается стабильным для всех endpoints одной версии API.
Можно использовать собственный JSON-конверт или формат problem details. Важнее не название полей, а единообразие, корректный статус и отсутствие чувствительных деталей.
Отключите HTML debug-страницы на production
Debug-режим полезен локально, но в production он раскрывает внутреннее устройство приложения и ломает контракт API. Проверяйте переменные окружения не только в web-процессе, но и в worker, контейнере и каждом экземпляре приложения.
- Отключите display_errors и подробные debug pages для публичного окружения.
- Сохраняйте полную ошибку в закрытом журнале с request ID.
- Не подавляйте исключение до состояния пустого ответа 500.
- Проверьте одинаковую конфигурацию всех replicas.
- Убедитесь, что журнал не содержит токены и пароли.
- Ограничьте доступ к служебным endpoints диагностики.
Найдите посторонний вывод до JSON
Даже правильный JSON становится невалидным, если перед ним выводится warning, notice, HTML из подключаемого файла, BOM или отладочная строка. Клиент видит символ < или другой неожиданный знак и сообщает об ошибке парсинга.
- Проверьте warning и deprecation после обновления PHP или библиотек.
- Удалите var_dump, print_r, echo и временные debug-вставки.
- Проверьте файлы на BOM и пробелы до открывающего PHP-тега.
- Не выводите шаблон ошибки из глобального include.
- Направляйте диагностические сообщения в журнал, а не в HTTP body.
- Проверьте, не добавляет ли HTML внешний модуль или middleware.
Проверьте Nginx, Apache, CDN и WAF
Приложение может правильно формировать JSON, но proxy заменяет его собственной страницей для 4xx или 5xx. Сравнение прямого ответа upstream и публичного домена помогает быстро локализовать этот слой.
- Отключите подмену ошибок proxy для namespace API или настройте JSON-ответ.
- Проверьте timeout, максимальный размер тела и доступность upstream.
- Передавайте исходный HTTP-статус без преобразования в 200.
- Не отправляйте API-запросы на frontend upstream из-за неверного location.
- Настройте исключения WAF аккуратно и только для подтвержденных ложных срабатываний.
- Добавьте request ID в proxy и приложение для сквозного поиска.
Не отключайте защиту целиком ради одного запроса. Сначала определите конкретное правило, полезную нагрузку и безопасный способ изменить конфигурацию.
Сделайте клиент устойчивым, но не скрывайте ошибку
Клиент не должен безусловно вызывать JSON parser для любого ответа. Сначала полезно проверить статус и Content-Type. Однако превращать любой HTML в пустой объект нельзя: это скроет сбой и может привести к неверному состоянию интерфейса.
- Проверяйте response.ok и ожидаемый диапазон статусов.
- Сверяйте Content-Type с ожидаемым форматом.
- Для диагностики сохраняйте только ограниченный безопасный фрагмент неожиданного ответа.
- Показывайте пользователю понятное сообщение без внутреннего HTML.
- Передавайте request ID в систему наблюдения.
- Не повторяйте автоматически небезопасную операцию без idempotency key.
Безопасный порядок исправления
- Воспроизведите один проблемный запрос и сохраните статус, заголовки и начало тела.
- Отключите автоматические redirect и определите первый ответ цепочки.
- Сравните публичный домен с прямым обращением к приложению.
- Определите источник HTML по заголовкам, request ID и журналам.
- Исправьте маршрут, middleware, exception handler или proxy на найденном уровне.
- Верните корректный статус и единый JSON-формат ошибки.
- Проверьте отсутствие stack trace, секретов и персональных данных.
- Добавьте контрактный тест для проблемного сценария.
- Повторите проверку с авторизацией, без нее и с неправильными данными.
- Разверните изменение с наблюдением за долей 4xx, 5xx и ошибками JSON parsing.
Как проверить результат
- Успешный ответ содержит ожидаемый JSON и корректный Content-Type.
- Ошибки 400, 401, 403, 404, 409 и 422 возвращаются в единой структуре.
- Неизвестный API route не открывает frontend или HTML-страницу.
- Внутренний сбой возвращает безопасный JSON с request ID и статусом 500.
- Proxy timeout и недоступность upstream не маскируются статусом 200.
- Клиент показывает понятную ошибку и не пытается разобрать HTML как JSON.
- Автоматические тесты проверяют и тело, и Content-Type, и HTTP-статус.
Типичные ошибки
- Добавить try/catch только в клиенте и оставить неправильный серверный ответ.
- Всегда возвращать 200 с полем success: false.
- Менять Content-Type на application/json, не меняя HTML-тело.
- Разрешить SPA fallback для всех /api/ маршрутов.
- Отдавать форму входа при истекшем API-токене.
- Включить подробный debug на production для поиска причины.
- Отключить WAF или proxy error handling целиком.
- Проверить только один endpoint и оставить другие форматы ошибок.
Как предотвратить повторение
- Опишите единый контракт ошибок и закрепите его в документации API.
- Добавьте общий exception handler и тесты для типовых статусов.
- Отделите маршруты API от web и SPA на уровне приложения и proxy.
- Проверяйте Content-Type и JSON schema в CI.
- Используйте request ID во всех слоях инфраструктуры.
- Отслеживайте рост HTML-ответов и ошибок JSON parsing на клиентах.
- Проверяйте production-конфигурацию после обновления framework, PHP или ingress.
Когда нужна помощь
Если API возвращает HTML вместо JSON, можно прислать URL endpoint без секретных параметров, метод, ожидаемый статус, безопасный фрагмент заголовков и время запроса. Я прослежу ответ от клиента до приложения, найду redirect, неверный route, debug-страницу или подмену proxy и настрою единый безопасный JSON-формат ошибок.