Проверяем транспорт до JSON
Начните с ответа на уровне HTTP, не вызывая JSON-декодер вслепую. Безопасно сохранять метод, публичный шаблон маршрута без query и идентификаторов в сегментах, статус, категорию Content-Type из разрешённого списка, факт редиректа и конечный origin, если он заранее разрешён политикой приложения. Не записывайте Authorization, cookies, API-ключи, полные URL с секретами и полные тела ошибок. Content-Type является полезным сигналом, но не доказывает, что тело действительно соответствует заявленному формату. Аналогично, статус 200 не гарантирует успешную бизнес-операцию: это нужно проверять по контракту конкретного API.
Что означает ошибка JSON-декодирования
JSONDecodeError сообщает о невозможности разобрать переданные байты или текст как JSON. Причиной может быть HTML-страница, текстовое сообщение gateway, обрезанное тело, пустой ответ или действительно некорректный JSON. Ошибка декодирования не устанавливает, какой компонент сформировал ответ, и не доказывает отказ модели или смену провайдера. Для диагностики сохраните только тип ошибки и безопасный контекст: статус, Content-Type, длину тела и, при необходимости, ограниченный отпечаток или локальный идентификатор fixture вместо содержимого.
Матрица синтетического воспроизведения
Подготовьте локальные fixtures без секретов и пользовательских данных: HTML со статусом 200, HTML с HTTP-ошибкой, корректный JSON, некорректный JSON и пустое тело. Для каждой записи отдельно проверяйте: был ли HTTP-статус допустимым, соответствует ли Content-Type ожидаемому, есть ли тело и успешно ли оно разбирается. Такая матрица помогает не смешивать транспортную ошибку с ошибкой формата. Пример ниже является иллюстративным и не заявляется как протестированный. Код рассчитан на Python 3.9+ и на учебный контракт, где успешный ответ обязан содержать application/json. HEAD, 204 и дополнительные JSON media types требуют собственной политики; пустое тело не всегда ошибка HTTP. Лимит 65 536 символов локальный, не лимит провайдера. NaN/Infinity отклоняются, глубокая вложенность выделяется отдельно. Разбор JSON ещё не проверяет бизнес-схему, обязательные поля или дубликаты ключей.
from dataclasses import dataclass
import json
from typing import Any, Optional
@dataclass
class SyntheticResponse:
status: int
content_type: Optional[str]
body: str
def classify_response(response: SyntheticResponse) -> dict[str, Any]:
if not 200 <= response.status < 300:
return {"kind": "http_error", "status": response.status}
if len(response.body) > 65536:
return {"kind": "body_too_large", "status": response.status}
if not response.body:
return {"kind": "empty_body", "status": response.status}
# This example contract explicitly permits only application/json.
content_type = (response.content_type or "").split(";", 1)[0].strip().lower()
if content_type != "application/json":
return {
"kind": "unexpected_content_type",
"status": response.status,
}
try:
def reject_non_json_constant(value):
raise ValueError("non-JSON numeric constant")
value = json.loads(response.body, parse_constant=reject_non_json_constant)
except (json.JSONDecodeError, ValueError):
return {"kind": "invalid_json", "status": response.status}
except RecursionError:
return {"kind": "json_resource_limit", "status": response.status}
return {"kind": "json", "status": response.status, "value_type": type(value).__name__}
fixtures = {
"html_200": SyntheticResponse(200, "text/html", "<html>synthetic</html>"),
"html_error": SyntheticResponse(502, "text/html", "<html>synthetic gateway error</html>"),
"valid_json": SyntheticResponse(200, "application/json", '{"ok": true}'),
"invalid_json": SyntheticResponse(200, "application/json", "{not-json}"),
"empty": SyntheticResponse(200, "application/json", ""),
}
expected = {"html_200": "unexpected_content_type", "html_error": "http_error",
"valid_json": "json", "invalid_json": "invalid_json", "empty": "empty_body"}
for name, response in fixtures.items():
assert classify_response(response)["kind"] == expected[name]
Разбираем маршрут, редирект и gateway
Если получен HTML, проверьте, не обращается ли клиент к web-маршруту вместо API-маршрута, не ведёт ли путь на страницу авторизации и не был ли запрос перенаправлен на другой origin. Отдельно сравните ожидаемый path с фактическим конечным URL и правилами reverse proxy. Редирект, страница входа, ответ балансировщика и ответ приложения — разные гипотезы, которые нельзя объявлять подтверждённым инцидентом без наблюдаемого статуса, заголовков и контролируемого воспроизведения. Не переходите по адресу, найденному в теле HTML, и не отключайте проверку TLS ради эксперимента.
Безопасная классификация для приложения
Пользователю достаточно сообщить категорию: HTTP-ошибка, неожиданный Content-Type, пустое тело или некорректный JSON. Внутренний журнал может содержать корреляционный идентификатор, статус, категорию Content-Type из allowlist и размер тела, но не секретные заголовки и не полный ответ. Не следует автоматически повторять запрос при любом JSONDecodeError: повтор допустим только при явно определённых, идемпотентных и временных условиях. Логика обработки должна учитывать контракт API, допустимые статусы и политику редиректов.
Границы вывода
По одному HTML-ответу нельзя надёжно определить, какой компонент его создал, был ли причиной прокси, неверный маршрут, авторизация или инфраструктурная ошибка. Для вывода нужны сопоставимые синтетические или разрешённые диагностические запросы, наблюдаемые HTTP-метаданные и проверка конфигурации маршрутизации. Не публикуйте токены, cookies, пользовательские запросы, полные тела ошибок или секретные заголовки даже в закрытых примерах без отдельной политики доступа и редактирования.
Шаблон промпта
Составь на русском технический материал для диагностики случая, когда HTTP-клиент ожидал JSON, но получил HTML или другой ответ. Объясни различие между JSONDecodeError и причиной неправильного ответа; используй RFC 9110 только для описания общей семантики HTTP status и representations, а документацию Python — для поведения JSON-декодера. Приведи локальную матрицу синтетических fixtures и безопасный пример классификации ответа, явно отметив, что пример не тестировался. Не выдумывай цены, бенчмарки, результаты испытаний, акции, совместимость или подтверждённый инцидент. Не публикуй токены, cookies, пользовательские запросы, полные HTML-тела, секретные заголовки и реальные ключи; для ключей используй переменные окружения. Отделяй наблюдения от гипотез о маршруте, редиректе, gateway и авторизации. Не советуй отключать TLS-проверку или отправлять учётные данные по адресу из HTML.