13. Формат ошибок — RFC 9457 Problem Details

Для кодов ответа НЕ 2xx используется единая структура по RFC 9457 Problem Details. Допустимо добавлять кастомные атрибуты под нужды проекта.

13.1 Обязательно

  • R-ERR-1. Тело ошибки соответствует структуре RFC 9457:

    {
      "type": "urn:problem:order-service: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"
    }
    

    Атрибуты:

    • type — стабильный URI/URN, идентифицирующий категорию ошибки. Подробнее в R-ERR-2.
    • status — HTTP-статус ответа.
    • title — короткое описание (обычно совпадает с названием статуса).
    • instance — уникальный идентификатор инцидента (URI/URN).
    • traceId — ID трассировки (из заголовка traceparent, см. R-HDR-4 в Заголовки).
    • code — символьный enum-код для программной логики. Все возможные коды ошибок указаны как enum в контракте.
    • detail — человекочитаемое сообщение для отображения пользователю (может быть на русском). Допустимо динамическое формирование по маске; допустимо несколько вариантов detail для одного code.
  • R-ERR-2. type — стабильный идентификатор категории ошибки. Одна и та же категория всегда возвращает один и тот же type. Это не ссылка на Swagger/OpenAPI, не JSON-схема, не динамический URL.

    Допустимые формы:

    1. URL на резолвимую страницу документации:

      "type": "https://errors.example.com/order/not-found"
      "type": "https://developer.example.com/errors/insufficient-stock"
      

      Страница описывает: что за ошибка, почему возникает, как исправить. Может располагаться на developer-портале, wiki, внутреннем портале документации.

    2. URN формы urn:problem:<service>:<code> — используется, если портала документации ошибок нет:

      "type": "urn:problem:order-service:order-not-found"
      "type": "urn:problem:catalog:product-archived"
      

      URN сохраняет машиночитаемую категорию ошибки и не требует deployment портала.

    Отношение к другим полям:

    • type — категория ошибки, стабильная (стандарт RFC).
    • code — enum для программной логики на клиенте (ORDER_NOT_FOUND). Удобная работа в коде без парсинга URI.
    • instance — уникальный URN конкретного инцидента, меняется при каждом вызове.
    • detail — текст для отображения пользователю.
  • R-ERR-3. Content-Type ответа с ошибкой — application/problem+json.

  • R-ERR-4. code формируется как UPPER_SNAKE_CASE. Все возможные коды перечислены в OpenAPI как enum.

    INTERNAL_SERVER_ERROR
    MISSING_DEFAULT_CARD
    APPLICATION_ALREADY_SENT
    ORDER_EMPTY
    EXT_SYSTEM_UNAVAILABLE
    
  • R-ERR-5. Ошибки валидации полей возвращаются с HTTP-кодом 400 Bad Request, code = VALIDATION_ERROR и расширением violations — массивом ошибок по конкретным полям:

    {
      "type": "urn:problem:order-service:validation-error",
      "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" }
      ]
    }
    
  • R-ERR-6. Правила формирования violations:

    • field — путь к полю в теле запроса. Dot-notation для вложенных объектов (deliveryAddress.zipCode), индексы для массивов (items[0].quantity).
    • message — человекочитаемое описание ошибки для отображения рядом с полем.
    • Массив содержит все ошибки валидации, а не только первую — клиент должен подсветить все невалидные поля за один запрос.
    • Если ошибка относится к объекту целиком (не к конкретному полю) — field не указывается или равен пустой строке.
  • R-ERR-7. OpenAPI-схема ProblemDetails и Violation. ENUM-список ErrorCode формируется один на контракт.

    ErrorCode:
      type: string
      description: 'Символьный код ошибки'
      enum:
        - INTERNAL_SERVER_ERROR
    
    ProblemDetails:
      type: object
      description: 'Problem Details (RFC 9457)'
      properties:
        type:
          type: string
          format: uri
          description: 'URI или URN, идентифицирующий тип ошибки'
        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: 'Почтовый индекс обязателен'
    
  • R-ERR-8. Все доступные ошибки указаны как examples в объектах ответа каждого метода (готовые YAML — в нижеследующей секции 13.3).

  • R-ERR-9. Используемые HTTP-коды ошибок:

    • 400 и 500 — всегда
    • 401 и 403 — если есть аутентификация и авторизация
    • 404 — если обращение к конкретному объекту по ID
    • 409 — конфликт при конкурентном обновлении (оптимистичная блокировка, дубликат)
    • 410 — для удалённых deprecated-эндпоинтов (см. R-DEP-3 в Rate limiting, файлы, deprecation)
    • 429 — при превышении лимита запросов (см. R-RATE-1 в Rate limiting, файлы, deprecation)

13.2 Запрещено

  • R-ERR-X1. Content-Type application/json для тела ошибки. Используй application/problem+json.

  • R-ERR-X2. type: "about:blank". Теряется машиночитаемая категория ошибки. Если портала документации нет — используй URN формы urn:problem:<service>:<code> (см. R-ERR-2).

  • R-ERR-X3. HTTP-коды ошибок вне списка R-ERR-9 (например, 418, 422, 451). Если возникает потребность — обсуждай в архитектурном комитете.

  • R-ERR-X4. Stack traces, SQL-запросы, внутренние пути в теле ошибки 500.

13.3 Examples в OpenAPI (для копирования в контракт)

400 Bad Request — всегда

"400":
  description: 'Bad Request'
  content:
    application/problem+json:
      schema:
        $ref: "#/components/schemas/ProblemDetails"
      examples:
        validation:
          summary: 'Ошибка валидации полей'
          value:
            type: "urn:problem:order-service:validation-error"
            status: 400
            title: "Bad Request"
            detail: "Ошибка валидации входных данных"
            code: VALIDATION_ERROR
            violations:
              - field: "amount"
                message: "Сумма должна быть больше 0"
              - field: "deliveryAddress.zipCode"
                message: "Почтовый индекс обязателен"
        malformed:
          summary: 'Некорректный формат запроса'
          value:
            type: "urn:problem:order-service:malformed-request"
            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: "urn:problem:order-service:token-expired"
            status: 401
            title: "Unauthorized"
            detail: "Токен доступа истёк. Получите новый токен."
            code: TOKEN_EXPIRED
        missing:
          summary: 'Токен отсутствует'
          value:
            type: "urn:problem:order-service:token-missing"
            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: "urn:problem:order-service:access-denied"
            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: "urn:problem:order-service:order-not-found"
            status: 404
            title: "Not Found"
            detail: "Заказ не найден"
            code: ORDER_NOT_FOUND

409 Conflict — при конкурентном обновлении или дубликате

"409":
  description: 'Conflict'
  content:
    application/problem+json:
      schema:
        $ref: "#/components/schemas/ProblemDetails"
      examples:
        concurrent:
          summary: 'Конкурентное обновление'
          value:
            type: "urn:problem:order-service:concurrent-modification"
            status: 409
            title: "Conflict"
            detail: "Ресурс был изменён другим запросом. Повторите операцию с актуальной версией."
            code: CONCURRENT_MODIFICATION
        duplicate:
          summary: 'Дубликат ресурса'
          value:
            type: "urn:problem:order-service:duplicate-order"
            status: 409
            title: "Conflict"
            detail: "Заказ с таким номером уже существует"
            code: DUPLICATE_ORDER

410 Gone — для удалённых deprecated-эндпоинтов

Эндпоинт удалён после прохождения даты Sunset (см. R-DEP-* в Rate limiting, файлы, deprecation).

"410":
  description: 'Gone'
  content:
    application/problem+json:
      schema:
        $ref: "#/components/schemas/ProblemDetails"
      examples:
        removed:
          summary: 'Эндпоинт удалён'
          value:
            type: "urn:problem:order-service:endpoint-removed"
            status: 410
            title: "Gone"
            detail: "Эндпоинт удалён. Используйте GET /api/v2/orders/{id}."
            code: ENDPOINT_REMOVED

429 Too Many Requests — если есть rate limiting

"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: "urn:problem:order-service:rate-limit-exceeded"
            status: 429
            title: "Too Many Requests"
            detail: "Превышен лимит запросов. Повторите через 30 секунд."
            code: RATE_LIMIT_EXCEEDED

500 Internal Server Error — всегда

"500":
  description: 'Internal Server Error'
  content:
    application/problem+json:
      schema:
        $ref: "#/components/schemas/ProblemDetails"
      examples:
        internal:
          summary: 'Внутренняя ошибка'
          value:
            type: "urn:problem:order-service:internal"
            status: 500
            title: "Internal Server Error"
            detail: "Внутренняя ошибка, попробуйте позже."
            code: INTERNAL_SERVER_ERROR
        extsystem:
          summary: 'Внешняя система недоступна'
          value:
            type: "urn:problem:order-service:ext-system-unavailable"
            status: 500
            title: "Internal Server Error"
            detail: "Внешняя система недоступна, попробуйте позже."
            code: EXT_SYSTEM_UNAVAILABLE

Все существующие бизнес-ошибки должны быть отражены в examples контракта. Указывать пример всех атрибутов, формируемых аналитиками: code, detail.