Когда 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.

Безопасный порядок исправления

  1. Воспроизведите один проблемный запрос и сохраните статус, заголовки и начало тела.
  2. Отключите автоматические redirect и определите первый ответ цепочки.
  3. Сравните публичный домен с прямым обращением к приложению.
  4. Определите источник HTML по заголовкам, request ID и журналам.
  5. Исправьте маршрут, middleware, exception handler или proxy на найденном уровне.
  6. Верните корректный статус и единый JSON-формат ошибки.
  7. Проверьте отсутствие stack trace, секретов и персональных данных.
  8. Добавьте контрактный тест для проблемного сценария.
  9. Повторите проверку с авторизацией, без нее и с неправильными данными.
  10. Разверните изменение с наблюдением за долей 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-формат ошибок.