REST API Style Guide: Ошибки RFC 9457
Формат ошибок REST API по RFC 9457 Problem Details. Violations, OpenAPI-схема.
REST API ошибки RFC 9457: 13. Формат ошибок -- RFC 9457 Problem Details
REST API ошибки RFC 9457 — для кодов ответа НЕ 2XX предусмотрена единая структура по RFC 9457 Problem Details. Content-Type ответа -- application/problem+json (не application/json). Допустимо добавлять новые персонализированные к проекту атрибуты.
13.1. Пример ответа
{
"type": "https://api.example.com/problems/validation-error",
"status": 400,
"title": "Validation Error",
"detail": "Поле amount должно быть больше 0.",
"instance": "urn:uuid:9f2d6c22-8e6d-4c2a-9b41-6b9a5e2f6c10",
"traceId": "00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01",
"code": "VALIDATION_ERROR"
}
13.2. Описание атрибутов
type-- генерируется. Стабильный URI, идентифицирующий категорию ошибки. Подробности в разделе 13.3. См. RFC 9457 — typestatus-- генерируется. HTTP-статус ответа. См. RFC 9457 — statustitle-- генерируется. Короткое описание (обычно совпадает с названием статуса). См. RFC 9457 — titleinstance-- генерируется. Уникальный идентификатор инцидента (URI/URN). См. RFC 9457 — instancetraceId-- генерируется. ID трассировкиcode-- формируют аналитики. Символьный статический код ошибки (enum), на который ориентируется потребитель API для разделения логики. Все возможные коды ошибок по всем методам должны быть указаны как enum в контрактеdetail-- формируют аналитики. Человекочитаемое сообщение об ошибке, предназначенное для отображения пользователю, в т.ч. на русском языке. См. RFC 9457 — detail- допустимо динамическое формирование detail по маске
- допустимо в detail указывать техническое разъяснение code
- допустимо для одного code несколько вариантов detail
13.3. Подробно о type
type -- это стабильный URI, идентифицирующий категорию ошибки, а не конкретный инцидент. Одна и та же категория ошибки всегда возвращает один и тот же type.
Что это НЕ является:
- Это не ссылка на Swagger/OpenAPI-спецификацию
- Это не ссылка на JSON-схему
- Это не динамический URL с параметрами запроса
Что это:
- Ссылка на человекочитаемую страницу документации, описывающую данный тип ошибки: что за ошибка, почему возникает, как исправить
- Страница может располагаться на внутреннем портале, wiki, developer portal
Примеры:
"type": "https://api.example.com/problems/order-empty"
"type": "https://api.example.com/problems/insufficient-stock"
"type": "https://developer.example.com/errors/missing-default-card"
# Если документации нет -- дефолт по RFC
"type": "about:blank"
Значение по умолчанию: если type не указан или равен about:blank, тип ошибки определяется только HTTP-статусом, а title должен совпадать с названием статуса (например, "Bad Request" для 400).
Отличие от других полей:
type-- категория ошибки, стабильная, одна на все случаи "заказ пустой". Назначение: ссылка на документацию + программная идентификация по RFCcode-- enum для программной логики на клиенте (ORDER_EMPTY). Назначение: удобная работа в коде без парсинга URIinstance-- уникальный URN конкретного инцидента, меняется при каждом вызовеdetail-- текст для отображения пользователю
type и code решают похожую задачу (идентификация типа ошибки), но type -- стандарт RFC, а code -- расширение контракта для удобства работы с enum.
Рекомендация: если в проекте нет портала документации ошибок, допустимо использовать about:blank. При наличии портала -- указывать реальный URI со страницей описания.
13.4. Правила формирования code
code формируется по маске {error-enum-code}. Используется для бизнес-ошибок и 500.
{error-enum-code}-- код ошибки, отражающий смысл. При нескольких словах -- UPPER_SNAKE_CASE
Примеры:
INTERNAL_SERVER_ERROR
MISSING_DEFAULT_CARD
APPLICATION_ALREADY_SENT
ORDER_EMPTY
EXT_SYSTEM_UNAVAILABLE
13.5. Ошибки валидации полей
При ошибках валидации формы/запроса клиенту нужно знать, какие именно поля невалидны, чтобы подсветить их в UI. Для этого в ProblemDetails добавляется расширение violations -- массив ошибок по конкретным полям.
HTTP-код: 400 Bad Request. Поле code: VALIDATION_ERROR.
Пример ответа:
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Ошибка валидации входных данных",
"instance": "urn:uuid:9f2d6c22-8e6d-4c2a-9b41-6b9a5e2f6c10",
"traceId": "00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01",
"code": "VALIDATION_ERROR",
"violations": [
{
"field": "amount",
"message": "Сумма должна быть больше 0"
},
{
"field": "deliveryAddress.zipCode",
"message": "Почтовый индекс обязателен"
},
{
"field": "items[0].quantity",
"message": "Количество должно быть от 1 до 99"
}
]
}
Правила формирования violations:
field-- путь к полю в теле запроса. Используется dot-notation для вложенных объектов (deliveryAddress.zipCode) и индексы для элементов массива (items[0].quantity)message-- человекочитаемое описание ошибки, предназначенное для отображения пользователю рядом с полем- Массив
violationsсодержит все ошибки валидации, а не только первую найденную -- клиент должен иметь возможность подсветить все невалидные поля за один запрос - Если ошибка относится к объекту целиком (не к конкретному полю),
fieldне указывается или равен пустой строке
OpenAPI-схемы ProblemDetails и Violation -- в разделе 13.7.
13.6. Используемые HTTP-коды ошибок
400и500-- всегда401и403-- если есть аутентификация и авторизация404-- если обращение к конкретному объекту по ID410-- для удалённых deprecated-эндпоинтов (см. раздел 16)429-- при превышении лимита запросов (см. раздел 14)- Другие коды НЕ используем
13.7. OpenAPI-схема (для копирования в контракт)
Указывать в ENUM все бизнесовые ошибки. ENUM-список формируется один на контракт.
ErrorCode:
type: string
description: 'Символьный код ошибки'
enum:
- INTERNAL_SERVER_ERROR
ProblemDetails:
type: object
description: 'Problem Details (RFC 9457)'
properties:
type:
type: string
format: uri
description: 'URI, идентифицирующий тип ошибки'
status:
type: integer
format: int32
description: 'HTTP статус'
title:
type: string
description: 'Краткое описание ошибки'
detail:
type: string
description: 'Подробности ошибки'
instance:
type: string
format: uri
description: 'URI конкретного экземпляра ошибки'
traceId:
type: string
code:
$ref: '#/components/schemas/ErrorCode'
violations:
type: array
description: 'Ошибки валидации по полям. Присутствует только при code=VALIDATION_ERROR'
items:
$ref: '#/components/schemas/Violation'
Violation:
type: object
required:
- message
properties:
field:
type: string
description: 'Путь к полю (dot-notation). Отсутствует если ошибка относится к объекту целиком'
example: 'deliveryAddress.zipCode'
message:
type: string
description: 'Описание ошибки для отображения пользователю'
example: 'Почтовый индекс обязателен'
13.8. Responses в контракте
Все доступные ошибки должны быть указаны как примеры в объектах ответа каждого метода. Ниже -- полный перечень для копирования.
400 Bad Request -- всегда
Ошибки валидации и некорректные запросы. Включает violations при ошибках полей формы.
"400":
description: 'Bad Request'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
validation:
summary: 'Ошибка валидации полей'
value:
type: "about:blank"
status: 400
title: "Bad Request"
detail: "Ошибка валидации входных данных"
code: VALIDATION_ERROR
violations:
- field: "amount"
message: "Сумма должна быть больше 0"
- field: "deliveryAddress.zipCode"
message: "Почтовый индекс обязателен"
malformed:
summary: 'Некорректный формат запроса'
value:
type: "about:blank"
status: 400
title: "Bad Request"
detail: "Невозможно разобрать тело запроса"
code: MALFORMED_REQUEST
401 Unauthorized -- если есть аутентификация
Токен отсутствует, истёк или невалиден.
"401":
description: 'Unauthorized'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
expired:
summary: 'Токен истёк'
value:
type: "about:blank"
status: 401
title: "Unauthorized"
detail: "Токен доступа истёк. Получите новый токен."
code: TOKEN_EXPIRED
missing:
summary: 'Токен отсутствует'
value:
type: "about:blank"
status: 401
title: "Unauthorized"
detail: "Заголовок Authorization отсутствует"
code: TOKEN_MISSING
403 Forbidden -- если есть авторизация
Пользователь аутентифицирован, но не имеет прав на операцию.
"403":
description: 'Forbidden'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
forbidden:
summary: 'Нет прав'
value:
type: "about:blank"
status: 403
title: "Forbidden"
detail: "Недостаточно прав для выполнения операции"
code: ACCESS_DENIED
404 Not Found -- если обращение к конкретному объекту по ID
Ресурс не найден. Также возвращается если ресурс существует, но не принадлежит текущему пользователю (чтобы не подтверждать существование чужих ресурсов).
"404":
description: 'Not Found'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
notfound:
summary: 'Объект не найден'
value:
type: "about:blank"
status: 404
title: "Not Found"
detail: "Заказ не найден"
code: ORDER_NOT_FOUND
410 Gone -- для удалённых deprecated-эндпоинтов
Эндпоинт был удалён после прохождения даты Sunset (см. раздел 16).
"410":
description: 'Gone'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
removed:
summary: 'Эндпоинт удалён'
value:
type: "about:blank"
status: 410
title: "Gone"
detail: "Эндпоинт удалён. Используйте GET /api/v2/orders/{id}."
code: ENDPOINT_REMOVED
429 Too Many Requests -- если есть rate limiting
Превышен лимит запросов. Заголовок Retry-After сообщает клиенту когда можно повторить.
"429":
description: 'Too Many Requests'
headers:
Retry-After:
schema:
type: integer
description: 'Секунд до сброса лимита'
RateLimit-Limit:
schema:
type: integer
description: 'Максимальное количество запросов в окне'
RateLimit-Remaining:
schema:
type: integer
description: 'Оставшееся количество запросов'
RateLimit-Reset:
schema:
type: integer
description: 'Unix timestamp сброса окна'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
ratelimit:
summary: 'Превышен лимит запросов'
value:
type: "about:blank"
status: 429
title: "Too Many Requests"
detail: "Превышен лимит запросов. Повторите через 30 секунд."
code: RATE_LIMIT_EXCEEDED
500 Internal Server Error -- всегда
Непредвиденная ошибка на сервере. Клиенту не возвращаются stack traces, SQL-запросы или внутренние пути.
"500":
description: 'Internal Server Error'
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
examples:
internal:
summary: 'Внутренняя ошибка'
value:
type: "about:blank"
status: 500
title: "Internal Server Error"
detail: "Внутренняя ошибка, попробуйте позже."
code: INTERNAL_SERVER_ERROR
extsystem:
summary: 'Внешняя система недоступна'
value:
type: "about:blank"
status: 500
title: "Internal Server Error"
detail: "Внешняя система недоступна, попробуйте позже."
code: EXT_SYSTEM_UNAVAILABLE
Все существующие бизнес-ошибки должны быть отражены в examples контракта. Указывать пример всех атрибутов, формируемых аналитиками: code, detail.