Материал подготовлен в преддверии старта курса «Системный аналитик. Экспертный уровень».

При разработке API естественно уделять основное внимание идеальному сценарию, в котором всё идет так, как надо. Разработчикам зачастую не нравится думать о сбоях, ведь, конечно же, всё должно работать как надо, поэтому ошибки нередко проектируются без той же тщательности, что и остальная часть API.

Ошибки в API — это не просто граничный случай. Это важная часть функциональности, и относиться к ним нужно как к ключевой функции, которую не стыдно показывать пользователям и документировать. Умение сообщать о сбоях ясно и кратко, возможно, важнее любого другого аспекта проектирования API.

Ошибки должны:

  • Быть максимально подробными.

  • Давать контекст: что именно пошло не так и почему.

  • Помогать людям узнать больше о проблеме.

  • Помогать программам понять, что делать дальше.

  • Оставаться единообразными во всем API.

Коды состояния HTTP

Путь к качественным ошибкам начинается с кодов состояния. Общепринятые соглашения по кодам состояния существуют для того, чтобы указывать, к какой категории относится ошибка, и это отличный способ помочь разработчикам автоматически принимать решения на их основе, например автоматически обновлять токены доступа при 403 или повторять запрос при 500.

Ошибки уровня приложения

Коды состояния HTTP лишь задают общую картину, то есть указывают категорию возникшей проблемы. Ошибка вроде 400 Bad Request обычно используется как расплывчатая универсальная ошибка, под которой скрывается целый ряд возможных проблем.

Чтобы помочь разработчикам понять, что именно пошло не так и как это исправить, не заставляя их копаться в журналах или обращаться в поддержку, нужно больше информации.

Подробности об ошибке полезны для:

  1. людей — чтобы разработчик, который строит интеграцию, мог понять проблему.

  2. программного обеспечения — чтобы клиентские приложения могли автоматически корректно обрабатывать большее число ситуаций.

Представьте, что вы разрабатываете приложение для совместных поездок, в котором пользователь планирует маршрут между двумя точками. Что произойдет, если он введет координаты, между которыми невозможно проехать, например между Англией и Исландией? Ниже приведена серия ответов API с возрастающей точностью:

HTTP/1.1 400 Bad Request

Не слишком полезный ответ об ошибке: пользователь не поймет, что именно он сделал неправильно.

HTTP/1.1 400 Bad Request
 
"error": {
    "message": "Trip is not possible, please check start/stop coordinates and try again."
}

Это сообщение можно показать пользователю, и оно поможет ему понять, как исправить проблему, но приложению будет очень трудно программно определить, что именно произошло и как на это реагировать.

HTTP/1.1 400 Bad Request
 
"error": {
    "code": "trip_not_possible",
    "message": "Trip is not possible, please check start/stop coordinates and try again."
}

Теперь здесь уже есть данные, которые помогают пользователям понять, что происходит, а также код ошибки, который позволяет при желании обработать ее программно.

Итак, всегда следует включать и сообщения об ошибках API, и коды ошибок API. Давайте подробнее разберем лучшие практики для каждого из этих элементов.

Сообщения об ошибках API

Сообщения об ошибках API должны быть ясными, краткими и включать в себя пояснение, что делать дальше. Они должны содержать достаточно информации, чтобы разработчик понял, что пошло не так и как это исправить.

Вот несколько лучших практик для сообщений об ошибках API:

  • Будьте конкретны: сообщение об ошибке должно ясно объяснять, что именно пошло не так.

  • Текст должен быть понятным человеку: сообщение об ошибке должно легко читаться и пониматься однозначно.

  • Делайте его практичным: сообщение об ошибке должно подсказывать, как исправить проблему.

  • Соблюдайте единообразие: сообщения об ошибках должны следовать единому формату во всем API.

Коды ошибок API

Использование кода ошибки давно стало устоявшейся практикой в экосистеме API. Однако, в отличие от кодов состояния, коды ошибок специфичны для конкретного API или организации. Тем не менее, и здесь есть соглашения, которых стоит придерживаться, чтобы формат кодов ошибок оставался предсказуемым.

Screenshot of Stripe.com API documentation's "Error Codes" page, which explains how "error codes" are added to provide extra information on top of HTTP status codes.
Скриншот страницы «Коды ошибок» в документации API Stripe.com, где объясняется, как добавляются «коды ошибок» для предоставления дополнительной информации помимо кодов состояния HTTP.

У кодов ошибок Stripe простая и понятная структура. У каждой ошибки есть код в виде строки и сообщение в виде строки, при этом это строковое значение задокументировано в сети, чтобы его можно было понять или передать в поддержку.

HTTP/1.1 400 Bad Request
 
{
  "error": {
    "code": "trip_too_short",
    "message": "This trip does not meet the minimum threshold for a carpool or 2 kilometers (1.24 miles)."
  }
}

Это также позволяет разработчикам легко программно реагировать на ошибку:

if (error.code === 'trip_too_short')

Полные объекты ошибок

Наличие кода и сообщения — уже хороший старт для сообщения об ошибке, но этого недостаточно, чтобы превратить ошибки в полезную возможность, а не просто в тревожный сигнал.

Вот что должна включать ошибка API (полный список):

  • Код состояния: указывает общую категорию ошибки (4xx для клиентских ошибок, 5xx для серверных).

  • Краткое описание: короткое, понятное человеку резюме проблемы, например: «Невозможно оформить заказ с пустой корзиной».

  • Подробное сообщение: более развернутое описание с дополнительным контекстом, например: «Была предпринята попытка оформить заказ, но в корзине нет товаров».

  • Специфичный для приложения код ошибки: уникальный код, который помогает разработчикам обрабатывать ошибку программно, например: cart-empty, ERRCARTEMPTY.

  • Ссылки на документацию: URL-адрес, по которому пользователи или разработчики могут найти дополнительную информацию

Некоторые предпочитают придумывать для этого собственный формат, но лучше оставить это профессионалам и использовать существующие стандарты: RFC 9457 — сведения о проблеме для HTTP API. Этот стандарт применяют всё больше команд, работающих с API.

{
  "type": "https://signatureapi.com/docs/v1/errors/invalid-api-key",
  "title": "Invalid API Key",
  "status": 401,
  "detail": "Please provide a valid API key in the X-Api-Key header."
}

В этом примере ошибки из Signature API есть поле type, которое по сути выполняет ту же роль, что и код ошибки. Но вместо произвольной строки вроде invalid-api-key стандарт рекомендует использовать URI, уникальный для конкретного API или экосистемы: https://signatureapi.com/docs/v1/errors/invalid-api-key. Такой URI не обязан куда-то вести, то есть при открытии он может никуда не перенаправлять, но может и вести. В последнем случае это заодно покрывает требование о «ссылке на документацию».

API Documentation for the SignatureAPI, with an explanation of what the error is, what happened, and how to fix it
Документация по API SignatureAPI с объяснением причины ошибки, ее характера и способов исправления.

Зачем нужны и title, и detail? Это позволяет использовать ошибку в веб-интерфейсе, где одни ошибки перехватываются и обрабатываются внутри системы, а другие передаются пользователю, чтобы ошибки воспринимались как часть функциональности, а не как что-то в духе: «Что-то пошло не так, эм, попробуйте еще раз или позвоните нам». Это может сократить число обращений в поддержку и помогает приложениям лучше развиваться, обрабатывая неизвестные проблемы еще до того, как интерфейс будет обновлен.

Вот более полный пример использования, включающий некоторые необязательные элементы стандарта и несколько расширений.

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
 
{
 "type": "https://example.com/probs/out-of-credit",
 "title": "Not enough credit.",
 "detail": "The current balance is 30, but that costs 50.",
 "instance": "/account/12345/msgs/abc",
 "balance": 30,
 "accounts": ["/account/12345", "/account/67890"]
}

В этом примере используются те же type, title и detail, но добавлены дополнительные поля.

Согласно RFC 9457, title — например, "Not enough credit." — должен быть одинаковым для всех проблем одного и того же типа ("https://example.com/probs/out-of-credit" в этом примере), тогда как detail может содержать сведения, относящиеся к конкретному случаю ошибки. Здесь detail показывает, что попытка списать 50 превышает текущий баланс 30.

Поле instance позволяет серверу указать на конкретный ресурс (или конечную точку), к которой относится ошибка. Такой URI, опять же, может разрешаться, поскольку здесь это относительный путь внутри API, а может и просто быть внутренним идентификатором, который не обязан реально существовать, но имеет смысл в контексте API. Это дает клиентам и пользователям возможность сообщать о конкретном случае проблемы с более содержательной формулировкой, чем просто «у меня не сработало…».

Поля balance и accounts не описаны в спецификации — это «расширения», то есть дополнительные данные, которые помогают клиентскому приложению корректно показать проблему пользователю. Это особенно полезно, если приложение предпочитает использовать эти значения для генерации собственных сообщений об ошибке вместо прямой вставки строк из title и detail, что дает больше возможностей для настройки и интернационализации.

Лучшие практики

Обработка ошибок при проектировании API — это нечто большее, чем просто выбор правильного кода состояния HTTP. Важно давать ясную и практичную информацию, которую смогут понять и использовать и разработчики, и приложения, и конечные пользователи этих приложений.

Вот еще несколько моментов, о которых стоит помнить при проектировании ошибок.

200 OK и код ошибки

Коды HTTP 4XX и 5XX сигнализируют клиенту, системам мониторинга, системам кэширования и множеству других сетевых компонентов, что произошло что-то плохое.

Ребята из CommitStrip.com прекрасно понимают, в чем тут проблема.

This monster has got his API responding with HTTP Status 200 OK despite the request failing.
(1) - Эй, кто-то сказал, что API не работал?
- Да ладно?
(2) - Хм, в логах сервера нет ошибок
- Вроде всё работает нормально...
(3) - Странно, я же специально сделал обработку ошибок, с сообщениями и всем остальным...
(4) - Посмотри-ка
- Да, вижу...
В чем смысл: сервер возвращает HTTP 200, то есть формально «всё хорошо», хотя внутри ответа прямо написано, что произошла ошибка 404.

Возврат кода состояния HTTP 200 вместе с кодом ошибки сбивает с толку каждого разработчика и каждый инструмент, который опирается на стандарты HTTP и когда-либо столкнется с этим API.

Некоторые считают HTTP «тупым каналом передачи данных», который нужен лишь для пересылки данных туда и обратно. Из этой логики следует, что если HTTP API сумел ответить, значит это 200 OK.

Это в корне ошибочный подход, но главная проблема в другом: вся работа по определению, был запрос успешным или завершился ошибкой, перекладывается на клиентский код. Инструменты кэширования закэшируют ошибку. Системы мониторинга не поймут, что возникла проблема. Всё будет выглядеть совершенно нормально, хотя по всей системе будет происходить непонятно что. Не делайте так.

Одна ошибка или несколько?

Должен ли API возвращать в ответе одну ошибку или несколько?

Некоторые хотят возвращать несколько ошибок, потому что перспектива исправить одну проблему, отправить запрос, снова получить ошибку, исправить следующую, снова отправить запрос и, возможно, опять получить ошибку кажется слишком утомительной.

Обычно всё упирается в то, как именно определяется сама ошибка. Да, конечно, клиенту было бы крайне неудобно получить один ответ с сообщением «у адреса электронной почты неверный формат», а после повторной отправки — еще один с сообщением «поле имени содержит недопустимые символы». Оба этих сообщения о проверке данных вполне можно было бы вернуть сразу, но для этого API не обязан возвращать несколько ошибок.

Ошибка здесь состоит в том, что «ресурс недопустим», и это может быть одной-единственной ошибкой. А сообщения о проверке данных — лишь дополнительная информация, приложенная к этой ошибке.

{
  "type": "https://example.com/probs/invalid-payload",
  "title": "The payload is invalid",
  "details": "The payload has one or more validation errors, please fix them and try again.",
  "validation": [
    {
      "message": "Email address is not properly formatted",
      "field": "email"
    },
    {
      "message": "Name contains unsupported characters",
      "field": "name"
    }
  ]
}

Такой подход предпочтителен, потому что невозможно заранее предусмотреть проблемы, которые могут возникнуть в части кода, до которой выполнение еще даже не дошло. Например, адрес электронной почты может быть корректным, но сервер электронной почты недоступен. Или имя может быть корректным, но база данных недоступна. Или такой адрес уже зарегистрирован. Всё это разные типы ошибок с разными кодами состояния, сообщениями и ссылками на документацию, которая по возможности помогает разобраться с каждой из них.

Собственный или стандартный формат ошибок

Если говорить о стандартах формата ошибок, то основных претендентов два:

RFC 9457 — сведения о проблеме для HTTP API

Самый новый и, пожалуй, лучший стандарт для HTTP-сообщений об ошибках. Единственная причина его не использовать — только если вы не знали о его существовании. Формально он новый, был выпущен в 2023 году, но приходит на смену RFC 7807 от 2016 года, который по сути описывает почти то же самое.

В нем много удачных идей, и его поддерживает всё больше инструментов: где-то напрямую через фрейморвки веб-приложений, а где-то через промежуточные обработчики и другие расширения.

Это помогает не изобретать велосипед, и по возможности этот стандарт настоятельно рекомендуется использовать.

Ошибки JSON:API

JSON:API — это не столько стандарт, сколько популярная спецификация, широко использовавшаяся во второй половине 2010-х годов. Она сосредоточена на том, чтобы задавать единый формат ответов для ресурсов, коллекций и связей, но в ней также есть вполне добротный формат ошибок, который многим нравится воспроизводить, даже если они не используют спецификацию целиком.

Выберите что-то одно

Уже давно тянется патовый сценарий: люди не внедряют стандартные форматы, пока не увидят поддержку со стороны большей части API-сообщества, или ждут, пока какой-нибудь крупный игрок не начнет продвигать стандарт. Но поскольку все ждут, что первым пойдет кто-то другой, в итоге не происходит ничего.

Выливается в итоге всё это в то, что каждый придумывает собственное решение, стандарт становится менее популярным, и круг замыкается.

Многие крупные компании могут позволить себе игнорировать такие стандарты, потому что способны разработать собственные эффективные внутренние стандарты и у них достаточно опытных специалистов, чтобы избежать большинства типичных проблем.

Небольшим командам, у которых нет такой привилегии, полезнее опираться на стандарты, написанные людьми с большим опытом и более широким пониманием задачи. Компании масштаба Facebook могут придумать собственный формат ошибок и буквально навязать свои решения всем остальным без особого сопротивления, но всем, кто работает в небольших командах, лучше придерживаться простых стандартов вроде RFC 9457, чтобы сохранять совместимость инструментов и не изобретать велосипед.

Retry-After

Проектировщики API стараются сделать его максимально удобным в использовании, поэтому, когда это уместно, стоит сообщать потребителям API, нужно ли повторить попытку позже, и если нужно, то когда именно. Для этого отлично подходит заголовок Retry-After.

HTTP/1.1 429 Too Many Requests
Retry-After: 120

Это сообщает клиенту, что перед новой попыткой нужно подождать две минуты. Здесь можно указывать либо метку времени, либо число секунд. Такой механизм помогает не допустить, чтобы клиент засыпал API запросами в тот момент, когда тому и без того тяжело.

Otus 9 лет! В честь события -15% на курс за прохождение теста до конца апреля
Otus 9 лет! В честь события -15% на курс за прохождение теста до конца апреля

Когда одной формулировки ошибки уже недостаточно, упираешься в более глубокую задачу: точно описывать требования, данные и взаимодействие систем. Курс «Системный аналитик. Экспертный уровень» как раз про это: он помогает системно прокачать проектирование API, интеграций, моделей данных и требований, чтобы принимать более сильные и предсказуемые решения в разработке. В честь дня рождения 1-4 апреля действует -10% по промокоду birthday (суммируется с другими скидками)

Пройдите тестирование по курсу, чтобы оценить свои знания и навыки. (до конца апреля за прохождение теста действует скидка -15%)

А для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:

  • 6 апреля в 20:00. «Ошибки системного аналитика при описании REST API». Записаться

  • 15 апреля в 20:00. «Джон, которого нет: Как микросервисы убивают целостность данных и что с этим делать системному аналитику». Записаться