Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья8 мин чтенияПросмотры0

application/problem+json: как не принять HTML-ошибку за JSON

Практический guide по RFC 9457: сначала проверяем HTTP-статус и Content-Type, затем безопасно разбираем Problem Details. На Python 3.12 воспроизводим корректный 422, HTML от шлюза, сломанный JSON, неверные типы и расхождение статусов.

Комментарии 0

Посылка на отдельном контрольном лотке рядом с янтарной сигнальной лампой и сканером
В этой статье

Клиент API не должен начинать с безусловного разбора JSON. Сначала он проверяет фактический HTTP-статус и базовый media type ответа. Только application/problem+json передаётся в обработчик Problem Details; HTML от прокси, пустое тело и сломанный JSON останавливаются как отдельные ошибки. Поле status внутри тела полезно для журнала, но не заменяет статус строки HTTP.

На маленьком примере импорт товара получает пять ответов: корректный 422, HTML от шлюза с 502, повреждённый JSON, поля неверных типов и расхождение между HTTP 503 и status 429 в теле. Один обработчик должен различить эти случаи, не показывая оператору трассировку и не принимая чужой формат за договорённый.

Что даёт Problem Details

RFC 9457 описывает объект Problem Details и media type application/problem+json. Формат нужен для машиночитаемых деталей ошибки и не отменяет семантику HTTP-статуса. Спецификация также предупреждает, что problem details не стоит превращать в отладочную свалку: внутренние сведения могут раскрыть устройство системы. Подробное описание полей приведено в RFC 9457.

  • type — основной идентификатор типа проблемы; при отсутствии подразумевается about:blank
  • title — короткое человекочитаемое название типа проблемы
  • status — копия HTTP-статуса для удобства, а не новый источник истины
  • detail — пояснение именно этого случая
  • instance — идентификатор конкретного возникновения проблемы

Расширения разрешены. Клиент, который их не знает, должен игнорировать их, а не отвергать весь ответ. Поэтому учебный trace_id не попадает в нормализованный результат: он не нужен общему обработчику.

Первый фильтр — Content-Type

Параметр charset допустим, поэтому сравниваем только базовый media type без учёта регистра и пробелов. Ответ text/html с кодом 502 не отправляем в json.loads: иначе настоящий сбой шлюза превратится в misleading JSONDecodeError и потеряет исходный статус.

HTTP и Content-Type

Действие клиента

Что сохранить

422; application/problem+json

Разобрать объект

HTTP 422, type, title, detail, instance

502; text/html

Не разбирать как JSON

HTTP 502 и безопасную причину отказа формата

500; application/problem+json, повреждённое тело

Остановить разбор

HTTP 500 и признак invalid problem JSON

503; в теле status 429

Отметить расхождение

HTTP 503, body status 429, mismatch

Минимальный проверяемый клиент

Проверка выполнена 18 октября 2026 года на CPython 3.12.14 только со стандартной библиотекой. Это чистая модель ответа: внешняя сеть, рабочий магазин и API 1С-Битрикс не используются.

from dataclasses import dataclass import json @dataclass(frozen=True) class Response: status: int content_type: str body: bytes def parse_problem(response: Response) -> dict: media_type = response.content_type.split(";", 1)[0].strip().lower() if media_type != "application/problem+json": raise ValueError(f"unexpected media type: {media_type or 'missing'}") try: raw = json.loads(response.body) except (UnicodeDecodeError, json.JSONDecodeError) as error: raise ValueError("invalid problem JSON") from error if not isinstance(raw, dict): raise ValueError("problem details must be a JSON object") def text(name: str, default: str | None = None) -> str | None: value = raw.get(name, default) return value if isinstance(value, str) else default body_status = raw.get("status") if not isinstance(body_status, int) or isinstance(body_status, bool): body_status = None return { "http_status": response.status, "type": text("type", "about:blank"), "title": text("title"), "detail": text("detail"), "instance": text("instance"), "body_status": body_status, "status_mismatch": body_status is not None and body_status != response.status, }

Функция принимает уже полученный ответ. Она не пытается автоматически открыть type URI, не выводит сырое тело пользователю и отбрасывает стандартные поля неверного типа. Булево значение для status тоже не принимается: в Python bool является подклассом int, поэтому это условие проверено отдельно.

Пять ответов и ожидаемый результат

cases = { "valid": Response( 422, "application/problem+json; charset=utf-8", json.dumps({ "type": "https://api.example.test/problems/invalid-stock", "title": "Invalid stock", "status": 422, "detail": "stock must be zero or greater", "instance": "/imports/7/items/42", "trace_id": "demo-123", }).encode(), ), "html_gateway": Response(502, "text/html", b"<h1>Bad Gateway</h1>"), "broken_json": Response(500, "application/problem+json", b'{"title":'), "wrong_types": Response( 400, "application/problem+json", b'{"type":7,"title":"Bad request","status":"400"}', ), "mismatch": Response( 503, "application/problem+json", b'{"title":"Busy","status":429}', ), } for name, response in cases.items(): try: problem = parse_problem(response) except ValueError as error: print(f"{name}: reject: {error}") else: print( f"{name}: accept: http={problem['http_status']} " f"type={problem['type']} body_status={problem['body_status']} " f"mismatch={problem['status_mismatch']}" )

valid: accept: http=422 type=https://api.example.test/problems/invalid-stock body_status=422 mismatch=False html_gateway: reject: unexpected media type: text/html broken_json: reject: invalid problem JSON wrong_types: accept: http=400 type=about:blank body_status=None mismatch=False mismatch: accept: http=503 type=about:blank body_status=429 mismatch=True

Скрипт завершился с кодом 0. Корректный 422 принят; HTML и повреждённый JSON отклонены до бизнес-обработки; неверные типы не разрушили весь объект; расхождение 503/429 стало явным флагом. Это наблюдения учебного запуска, а не измерение конкретного внешнего сервиса.

Три пути обработки ответа API: Problem Details разбирается, HTML отклоняется, расхождение статусов отмечается
Учебная схема пяти тестов: формат ответа проверяется до JSON, а HTTP-статус сохраняется отдельно от поля status в теле.

Почему status в теле не должен управлять повтором

RFC называет status в объекте advisory. Промежуточный узел может изменить HTTP-статус, а сохранённое тело — остаться прежним. Если сеть сообщает 503, а JSON утверждает 429, клиент не должен молча выбрать удобное число. Он сохраняет оба значения, отмечает расхождение и применяет политику по фактическому HTTP-ответу и контракту интеграции.

Для паузы после 429 нужен отдельный разбор заголовка Retry-After. Он проверен в материале 429 Retry-After: секунды или дата. Problem Details может пояснить ошибку, но не заменяет правила повторов.

Как встроить в интеграцию магазина

Для 1С-Битрикс: Управление сайтом это не обещание встроенного формата конкретного метода. Сначала в контракте явно указывают внешний API, версию, операцию, права, допустимые HTTP-статусы и Content-Type. Если поставщик возвращает собственную схему ошибок, под неё нужен отдельный адаптер; переименовать поля в Problem Details на стороне клиента недостаточно.

  • Транспортный слой сохраняет HTTP-статус, Content-Type и ограниченный диагностический идентификатор без секретов.
  • Диспетчер выбирает обработчик по базовому media type.
  • Обработчик Problem Details валидирует JSON-объект и типы стандартных полей.
  • Бизнес-слой принимает решение по HTTP-статусу, type и документированным расширениям.
  • Неизвестный type или расширение не запускает догадки: пакет останавливается либо уходит в ручную проверку.
  • Сырое тело и instance не выводятся покупателю без проверки на персональные и внутренние данные.

Протокол приёмки

  • Подать корректный application/problem+json с параметром charset; ожидать сохранённые HTTP-статус, type и detail.
  • Подать text/html с 502; убедиться, что JSON-парсер не вызван и исходный HTTP-статус не потерян.
  • Подать повреждённый JSON с правильным media type; ожидать отдельную ошибку invalid problem JSON.
  • Подать массив вместо объекта и стандартные поля неверных типов; массив отклонить, неверные поля игнорировать по контракту.
  • Подать несовпадающие HTTP status и поле status; сохранить оба значения и поднять mismatch.
  • Добавить неизвестное расширение; убедиться, что базовые поля продолжают обрабатываться.
  • Проверить журнал: в нём нет токенов, cookie, полного HTML, персональных данных и трассировки стека.

Границы примера

Учебный код не выполняет HTTP-запрос, не проверяет TLS, размер тела, кодировку Content-Encoding и лимиты памяти. В рабочем клиенте они проверяются до разбора. Также нужен предел для detail и журналирования, иначе корректный media type всё ещё может принести слишком большой или чувствительный текст.

Problem Details помогает унифицировать форму ошибки, но не исправляет неверный статус, не задаёт стратегию повтора и не заменяет документацию problem type. Критерий готовности простой: каждый неожиданный формат или конфликт статусов остаётся видимым, а не превращается в случайное исключение JSON-парсера.

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.