Реализация 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