Реализация 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.ЭтоПроблема(ОтветСервиса) Тогда
...
КонецЕсли;| Метод | Возвращает | Описание |
|---|---|---|
Новая(Статус, Детали = Неопределено) | 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(КодСостояния = Неопределено, Пояснение = Неопределено).
| Установщик | Член | Описание |
|---|---|---|
ТипПроблемы(Значение) | type | URI-ссылка, опознающая род проблемы |
Заголовок(Значение) | title | Короткое описание рода проблемы |
Статус(Значение) | status | Код состояния HTTP, целое 100..599 |
Детали(Значение) | detail | Пояснение для конкретного случая |
Экземпляр(Значение) | instance | URI-ссылка на конкретный случай |
Расширение(Имя, Значение) | - | Дополнительный член верхнего уровня |
Каждый возвращает ЭтотОбъект. Неопределено убирает член из документа.
Геттеры: ПолучитьТип(), ПолучитьЗаголовок(), ПолучитьСтатус(), ПолучитьДетали(), ПолучитьЭкземпляр(), ПолучитьРасширение(Имя, ЗначениеПоУмолчанию = Неопределено), ПолучитьРасширения().
Вывод: ВСтруктуру(), ВJson(СОтступами = Ложь), ТипСодержимого().
Установщик члена type называется ТипПроблемы, а не Тип: «Тип» - зарезервированное слово языка, методом оно быть не может.
| Метод | Возвращает | Описание |
|---|---|---|
Сформировать(Проблема, СОтступами = Ложь) | Структура | Данные ответа по готовому документу |
ПоСтатусу(Статус, Детали = Неопределено, СОтступами = Ложь) | Структура | Данные ответа сразу по коду состояния |
Результат - Структура с полями КодСостояния (Число), Заголовки (Соответствие) и Тело (Строка).
Спецификация: 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 и вообще ни от какого веб-фреймворка: ProblemResponse возвращает нейтральную Структуру, а перекладывает её в ответ уже вызывающий код. Так пакет остаётся пригоден и для winow, и для любого другого сервера, а список зависимостей - пустым.
Данные= ProblemResponse.Сформировать(HttpProblems.НеНайдено("Флаг не найден"));Ответ=Новый ВебОтвет(Данные.КодСостояния);Для Каждого Заголовок Из Данные.Заголовки Цикл
Ответ.Заголовки.Вставить(Заголовок.Ключ, Заголовок.Значение);КонецЦикла;
Ответ.УстановитьТелоИзСтроки(Данные.Тело);Возврат Ответ;Спецификация требует, чтобы код в ответе совпадал с членом status, - Сформировать берёт его из документа. Если статус в документе не задан, взять его неоткуда, и используется 500.
Заголовки вроде WWW-Authenticate для 401 или Retry-After для 429 к самому документу не относятся: их проставляет вызывающий код.
opm install -l
oneunit execute -d ./tests