REST API Style Guide: Ошибки RFC 9457

Формат ошибок REST API по RFC 9457 Problem Details. Violations, OpenAPI-схема.

Статья внедрена в скилл AI-агента ucp-api-review / ucp-api-design REST API ошибки RFC 9457

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 — type
  • status -- генерируется. HTTP-статус ответа. См. RFC 9457 — status
  • title -- генерируется. Короткое описание (обычно совпадает с названием статуса). См. RFC 9457 — title
  • instance -- генерируется. Уникальный идентификатор инцидента (URI/URN). См. RFC 9457 — instance
  • traceId -- генерируется. 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 -- категория ошибки, стабильная, одна на все случаи "заказ пустой". Назначение: ссылка на документацию + программная идентификация по RFC
  • code -- enum для программной логики на клиенте (ORDER_EMPTY). Назначение: удобная работа в коде без парсинга URI
  • instance -- уникальный 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 -- если обращение к конкретному объекту по ID
  • 410 -- для удалённых 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.