Skip to content
 
 

Repository files navigation

problem-details

telegram chat Ask 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 Документ по коду состояния
НекорректныйЗапрос(Детали = Неопределено) ProblemDetails 400 Bad Request
НеАутентифицирован(Детали = Неопределено) ProblemDetails 401 Unauthorized
Запрещено(Детали = Неопределено) ProblemDetails 403 Forbidden
НеНайдено(Детали = Неопределено) ProblemDetails 404 Not Found
Конфликт(Детали = Неопределено) ProblemDetails 409 Conflict
ОшибкаВалидации(Нарушения = Неопределено, Детали = Неопределено) ProblemDetails 422 + расширение errors
СлишкомМногоЗапросов(Детали = Неопределено) ProblemDetails 429 Too Many Requests
ВнутренняяОшибка(Детали = Неопределено) ProblemDetails 500 Internal Server Error
Нарушение(Детали, Указатель = Неопределено) Структура Одно нарушение валидации
Разобрать(ТекстJson) ProblemDetails Разбор тела ответа
ЭтоПроблема(ТекстJson) Булево Проверка без выброса исключения
ФразаСтатуса(Код) Строка Стандартная фраза кода по реестру IANA
КодыСтатусов() Массив Коды, для которых известна фраза
ТипСодержимого() Строка application/problem+json

Класс ProblemDetails

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

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

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

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

Вывод: ВСтруктуру(), В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