Skip to content

Repository files navigation

problem-details

telegram chatAsk DeepWiki

Реализация RFC 9457 «Problem Details for HTTP APIs» для OneScript - стандартное машиночитаемое тело ответа с ошибкой.

Кода состояния часто недостаточно: 403 не объясняет, почему именно отказано, а 422 не говорит, какое поле не прошло проверку. Вместо самодельного формата ошибки API отдаёт документ с медиатипом application/problem+json, который клиент разбирает по одним и тем же правилам у любого сервиса.

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30
}

Род проблемы опознаётся по type, конкретный случай - по instance, а balance - расширение, которое этот тип проблемы определил сам. RFC 9457 обновляет RFC 7807 и совместим с ним по формату.

Установка

opm install problem-details

Использование

Ответ об ошибке в одну строку

#Использовать problem-details
Проблема= HttpProblems.НеНайдено("Флаг new-checkout не зарегистрирован");Сообщить(Проблема.ВJson());// {"type":"about:blank","title":"Not Found","status":404,// "detail":"Флаг new-checkout не зарегистрирован"}

Заголовок подставился сам: тип остался about:blank, а для него спецификация предписывает брать title из стандартной фразы кода состояния.

Именованные фабрики есть для частых кодов, для остальных - Новая:

HttpProblems.Новая(451,"Ресурс недоступен по требованию регулятора");

Свой тип проблемы

Проблема=Новый ProblemDetails(403)
.ТипПроблемы("https://example.com/probs/out-of-credit")
.Заголовок("You do not have enough credit.")
.Детали("Your current balance is 30, but that costs 50.")
.Экземпляр("/account/12345/msgs/abc")
.Расширение("balance",30);

Установщики возвращают сам объект, поэтому документ собирается цепочкой. В расширение можно положить любое сериализуемое значение - число, массив, вложенную структуру.

Ошибки валидации

Спецификация приводит пример расширения errors - массива нарушений, каждое с пояснением и указателем на место в обращении. ОшибкаВалидации собирает его из удобного описания полей:

Поля=Новый Соответствие();
Поля.Вставить("age","must be a positive integer");
Поля.Вставить("profile/color","must be 'green', 'red' or 'blue'");Сообщить(HttpProblems.ОшибкаВалидации(Поля).ВJson(Истина));
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"errors": [
{ "detail": "must be a positive integer", "pointer": "#/age" },
{ "detail": "must be 'green', 'red' or 'blue'", "pointer": "#/profile/color" }
]
}

Нарушения принимаются в любом из видов: соответствие или структура «поле → сообщение» (или «поле → массив сообщений»), массив строк, массив готовых нарушений от HttpProblems.Нарушение, одна строка.

Разбор ответа

Проблема= HttpProblems.Разобрать(ОтветСервиса);Если Проблема.ПолучитьСтатус() =429Тогда
Приостановить(Проблема.ПолучитьРасширение("retryAfter",60) *1000);КонецЕсли;

Разбор снисходителен, как того требует спецификация: отсутствующий член остаётся незаданным, член неподходящего типа игнорируется, незнакомые члены становятся расширениями. Исключение выбрасывается только если текст вообще не является объектом JSON - проверить это заранее можно через ЭтоПроблема:

Если HttpProblems.ЭтоПроблема(ОтветСервиса) Тогда
...
КонецЕсли;

Публичный API

Модуль HttpProblems

МетодВозвращаетОписание
Новая(Статус, Детали = Неопределено)ProblemDetailsДокумент по коду состояния
НекорректныйЗапрос(Детали = Неопределено)ProblemDetails400 Bad Request
НеАутентифицирован(Детали = Неопределено)ProblemDetails401 Unauthorized
Запрещено(Детали = Неопределено)ProblemDetails403 Forbidden
НеНайдено(Детали = Неопределено)ProblemDetails404 Not Found
Конфликт(Детали = Неопределено)ProblemDetails409 Conflict
ОшибкаВалидации(Нарушения = Неопределено, Детали = Неопределено)ProblemDetails422 + расширение errors
СлишкомМногоЗапросов(Детали = Неопределено)ProblemDetails429 Too Many Requests
ВнутренняяОшибка(Детали = Неопределено)ProblemDetails500 Internal Server Error
Нарушение(Детали, Указатель = Неопределено)СтруктураОдно нарушение валидации
Разобрать(ТекстJson)ProblemDetailsРазбор тела ответа
ЭтоПроблема(ТекстJson)БулевоПроверка без выброса исключения
ФразаСтатуса(Код)СтрокаСтандартная фраза кода по реестру IANA
КодыСтатусов()МассивКоды, для которых известна фраза
ТипСодержимого()Строкаapplication/problem+json

Класс ProblemDetails

Конструктор: Новый ProblemDetails(КодСостояния = Неопределено, Пояснение = Неопределено).

УстановщикЧленОписание
ТипПроблемы(Значение)typeURI-ссылка, опознающая род проблемы
Заголовок(Значение)titleКороткое описание рода проблемы
Статус(Значение)statusКод состояния HTTP, целое 100..599
Детали(Значение)detailПояснение для конкретного случая
Экземпляр(Значение)instanceURI-ссылка на конкретный случай
Расширение(Имя, Значение)-Дополнительный член верхнего уровня

Каждый возвращает ЭтотОбъект. Неопределено убирает член из документа.

Геттеры: ПолучитьТип(), ПолучитьЗаголовок(), ПолучитьСтатус(), ПолучитьДетали(), ПолучитьЭкземпляр(), ПолучитьРасширение(Имя, ЗначениеПоУмолчанию = Неопределено), ПолучитьРасширения().

Вывод: ВСтруктуру(), ВJson(СОтступами = Ложь), ТипСодержимого().

Установщик члена type называется ТипПроблемы, а не Тип: «Тип» - зарезервированное слово языка, методом оно быть не может.

Модуль ProblemResponse

МетодВозвращаетОписание
Сформировать(Проблема, СОтступами = Ложь)СтруктураДанные ответа по готовому документу
ПоСтатусу(Статус, Детали = Неопределено, СОтступами = Ложь)СтруктураДанные ответа сразу по коду состояния

Результат - Структура с полями КодСостояния (Число), Заголовки (Соответствие) и Тело (Строка).

Соответствие RFC 9457

Спецификация: https://www.rfc-editor.org/rfc/rfc9457.html

  • Медиатип - application/problem+json.
  • Стандартные члены type, title, status, detail, instance сериализуются в порядке спецификации, расширения - после них в порядке добавления. Незаданный член в документ не попадает: по спецификации отсутствие члена и есть «нет значения».
  • type присутствует в документе всегда. Отсутствующий член равнозначен about:blank (раздел 3.1.1), поэтому явная запись значения по умолчанию ничего не меняет по смыслу, зато тело остаётся самодостаточным, если его сохранили отдельно от HTTP-ответа.
  • Когда type равен about:blank, а status задан, title берётся из стандартной фразы кода состояния - как предписывает раздел 4.2.1. Явно заданный заголовок не перекрывается; при своём типе проблемы фраза не подставляется, потому что заголовок должен описывать этот тип, а не код ответа.
  • status проверяется как целое число из диапазона 100..599 (приложение A). Значение вне диапазона - ошибка при сборке документа и молча игнорируемый член при разборе.
  • Расширения - произвольные члены верхнего уровня. Имя должно начинаться с буквы и состоять из букв, цифр и _ - именно это рекомендует раздел 4 (чтобы имя годилось и для форматов, отличных от JSON). Стандартный член расширением задать нельзя.
  • Разбор устойчив: член отсутствует - значит не задан, тип значения не совпал с ожидаемым - член игнорируется (раздел 3.1), незнакомый член становится расширением. Расширения с именами, непредставимыми в виде члена документа (например foo-bar), при разборе отбрасываются: раздел 3.2 прямо разрешает потребителю игнорировать нераспознанные расширения.
  • Расширение errors для нарушений валидации повторяет пример из раздела 3: массив объектов с detail и pointer, где указатель записан фрагментом с JSON Pointer (#/age).
  • Таблица фраз статусов повторяет реестр IANA целиком (снимок от 2025-09-15) - 62 кода от 1xx до 5xx. Коды 306 и 418 фразы не имеют: реестр помечает их как «(Unused)». Для 510 фраза - Not Extended, пометка «OBSOLETED» относится к регистрации, а не к самой фразе. Для незанятых кодов ФразаСтатуса возвращает Неопределено, и тогда заголовок просто не выводится.
  • Эквивалентный XML-формат (application/problem+xml, приложение B) не реализован.

Интеграция с winow

Пакет не зависит от winow и вообще ни от какого веб-фреймворка: ProblemResponse возвращает нейтральную Структуру, а перекладывает её в ответ уже вызывающий код. Так пакет остаётся пригоден и для winow, и для любого другого сервера, а список зависимостей - пустым.

Данные= ProblemResponse.Сформировать(HttpProblems.НеНайдено("Флаг не найден"));Ответ=Новый ВебОтвет(Данные.КодСостояния);Для Каждого Заголовок Из Данные.Заголовки Цикл
Ответ.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);КонецЦикла;
Ответ.УстановитьТелоИзСтроки(Данные.Тело);Возврат Ответ;

Спецификация требует, чтобы код в ответе совпадал с членом status, - Сформировать берёт его из документа. Если статус в документе не задан, взять его неоткуда, и используется 500.

Заголовки вроде WWW-Authenticate для 401 или Retry-After для 429 к самому документу не относятся: их проставляет вызывающий код.

Тесты

opm install -l
oneunit execute -d ./tests

Лицензия

MIT

About

Стандартное тело HTTP-ошибки application/problem+json: построение, разбор и данные ответа

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages