API
Статус-коды, которые реально важны в проде
Платёжный шлюз начал падать на апстриме во время пятничного деплоя. Все дашборды оставались зелёными: success rate 100%, error rate 0%, ни одного алерта. Дежурный инженер узнал об инциденте из твитов клиентов, а не из мониторинга. Причина — одна строка: шлюз ловил падения апстрима и возвращал 200 OK с { "error": "upstream timeout" } в теле. Метрики считали HTTP-статус, поэтому полный отказ сорок минут выглядел как идеальное здоровье.
Классы — это решение о маршрутизации, а не зубрёжка
Запомнить, что 404 — это «не найдено», уровень джуна. Взгляд сеньора: первая цифра — это инструкция маршрутизации для машин, которые никогда не читают тело ответа: кэш, балансировщик, APM-агент, цикл ретраев клиента. Они ветвятся по классу, и ветвятся сразу.
- 2xx — сработало; ответ кэшируем по своим заголовкам, считай успехом.
- 3xx — иди в другое место; следуй
Locationили используй кэш. - 4xx — ты, клиент, прислал что-то не то; не ретрай тот же запрос, он упадёт точно так же.
- 5xx — я, сервер, упал; запрос мог быть валидным, поэтому ретрай может сработать.
Раздел 4xx/5xx — несущий. Это разница между «стоп, ты сломал» и «подожди, я сломал, попробуй снова». Ошибись классом — и каждая машина ниже по потоку примет неверное решение: кэш сохранит отказ, цикл ретраев будет долбить запрос, который никогда не пройдёт, дашборд просчитается с инцидентом.
2xx — не одно и то же: 200 vs 201 vs 202 vs 204
Класс успеха несёт реальную информацию, которой сеньор формирует контракт API.
200 OK— готово, вот результат. Синхронно, полностью.201 Created— теперь существует ресурс; верни заголовокLocationс указанием на него. Это правильный ответ на успешныйPOST /orders, а не голый 200.202 Accepted— «я принял твой запрос, но он ещё не выполнен». Работа в очереди или асинхронна. Тело должно вернуть способ опросить статус. Вернуть тут 200 — ложь: клиент думает, что заказ ушёл, а он ещё лежит в очереди.204 No Content— успех, и сознательно нечего возвращать (DELETEилиPUT, который не изменил ничего, что клиенту надо отразить). Экономит раунд-трип на парсинг пустого тела.
Различие 202-vs-200 — там, где текут асинхронные системы. Если эндпоинт ставит работу в Kafka и возвращается сразу, 202 говорит клиенту «опрашивай меня». 200 говорит «мы закончили» — и теперь его UI показывает подтверждённый заказ, которого ещё нигде нет.
Словарь 4xx: 400 vs 422 vs 409 vs 412 и 401 vs 403
Здесь большинство API неряшливы — и здесь точный клиент может дать полезное сообщение об ошибке вместо общего «что-то пошло не так».
| Код | Значит | Что должен сделать клиент |
|---|---|---|
400 Bad Request | Малформед — битый JSON, нет обязательного поля, не парсится | Исправь структуру запроса; не ретрай как есть |
422 Unprocessable | Распарсилось, но значения нарушают бизнес-правила (битый email, qty < 0) | Исправь значения; покажи ошибки по полям |
409 Conflict | Конфликт с текущим состоянием ресурса (дубль, гонка версий) | Перечитай состояние, разреши, переотправь |
412 Precondition Failed | If-Match/If-Unmodified-Since дали ложь | Перефетчи сущность (ETag сдвинулся); ретрай с новым precondition |
401 Unauthorized | Не аутентифицирован — нет/протух/невалиден креденшл | Залогинься или обнови токен, затем ретрай |
403 Forbidden | Аутентифицирован, но делать это не разрешено | Стоп — смена креденшла не поможет |
RFC 9110 проводит 400 как синтаксис, а 422 как семантику: 400 — «я не смог это распарсить», 422 — «я распарсил, и значения неверны». Граница размытая, и многие API сваливают оба в 400, но различие стоит держать — оно говорит клиенту, чинить форму или данные, а это разница между ошибкой разработчика и ошибкой пользователя.
Пару 401 vs 403 постоянно путают. 401 значит «я не знаю, кто ты» — фикс в аутентификации. 403 значит «я точно знаю, кто ты, и тебе всё равно нельзя» — обновление токена не поможет. Вернуть 401 на отказ авторизации загоняет клиентов в бессмысленный цикл релогина.
Почему это работает
Есть нюанс безопасности на 404 vs 403. Строгий 403 Forbidden на записи, которую пользователю видеть не положено, подтверждает, что запись существует, — это утечка информации. Для чувствительных ресурсов (чужой счёт по id) сеньоры часто возвращают 404 Not Found, чтобы атакующий, перебирающий id, не отличал «такой записи нет» от «есть, но не твоя». Ты меняешь корректность REST на то, что не утекает факт существования.
5xx: чья вина и что клиент делает дальше
Класс 5xx — там, где живёт стратегия ретраев, а под-коды распределяют вину.
500Internal Server Error — твоё приложение бросило исключение и не обработало. Баг в твоём коде.502Bad Gateway — прокси/балансировщик получил малформед или ничего от апстрима, который вызвал. Сам шлюз цел; что-то за ним сломано.503Service Unavailable — сервер сознательно не обслуживает: перегружен, дренится, на обслуживании. Это единственный 5xx, который должен нести заголовокRetry-After.504Gateway Timeout — шлюз ждал апстрим и сдался. Запрос мог завершиться на бэкенде, хотя клиент увидел таймаут, — именно поэтому слепой ретрай неидемпотентного вызова тут опасен.
Рефлекс сеньора: 5xx ретраебельно (запрос мог быть валидным, а сбой временным), 4xx нет (запрос неверен и упадёт точно так же) — с 429 как явным ретраебельным исключением в диапазоне 4xx.
429 и Retry-After: бэкофф, которым управляет сервер
429 Too Many Requests говорит, что ты упёрся в rate limit. Правильный ответ несёт заголовок Retry-After — либо число секунд (Retry-After: 30), либо HTTP-дату. Сервер знает своё окно лимита; уважай его. Откатывайся к клиентскому экспоненциальному бэкоффу с джиттером (1с, 2с, 4с, 8с…) только когда Retry-After нет. Google Cloud Storage, OpenAI и Stripe документируют ровно этот порядок: сначала читай Retry-After, потом бэкофф с джиттером.
Правило ретраев, которое всё связывает — и которое вызывает реальные инциденты при нарушении: ретрай 5xx и 429; никогда слепо не ретрай неидемпотентный 4xx или неидемпотентный запрос после таймаута. GET, PUT, DELETE идемпотентны — ретрай безопасен. POST — нет. Если POST /charge ловит таймаут (504) и клиент ретраит, списание может пройти дважды. Фикс — ключ идемпотентности: Stripe и Square требуют от клиентов слать уникальный ключ на логическую операцию, чтобы сервер дедуплицировал ретраи вместо двойного списания.
Клиент делает POST /charge, запрос ловит таймаут 504, ключ идемпотентности не отправлен. Списание могло пройти, а могло и нет. Что делает корректный клиент?
Эндпоинт ставит работу в Kafka и возвращается до её завершения. Что он должен вернуть?
Пользователь залогинен, но пытается удалить запись чужого тенанта. Какой статус корректен?
Клиент получил ответ с ошибкой. Расставь решения, определяющие, ретраить ли и как:
- 1 Прочитай класс статуса: это 4xx (ошибка клиента) или 5xx/429 (ретраебельно)?
- 2 Если 4xx и не 429: не ретрай — запрос неверен, упадёт точно так же
- 3 Если 5xx или 429: проверь, идемпотентен ли запрос или несёт ли ключ идемпотентности
- 4 Если идемпотентен/с ключом: уважай Retry-After если есть, иначе экспоненциальный бэкофф с джиттером
- 5 Если неидемпотентен и без ключа (напр. POST после 504): не ретрай слепо — сверяйся вместо этого
- 01Объясни, почему возвращать 200 OK с объектом ошибки в теле опасно и что это ломает ниже по потоку.
- 02Пройди по решению о ретрае упавшего запроса, включая ловушку идемпотентности.
Статус-код — это контракт, который три машины читают раньше любого человека: кэши, мониторинг и логика ретраев, — а первая цифра — инструкция, по которой они ветвятся. 2xx не монолитен: 201 с Location для создания, 202 для принятой-но-асинхронной работы, 204 для сознательно пустого успеха. Словарь 4xx несёт реальный смысл, которым пользуется точный клиент — 400 для малформед-синтаксиса, 422 для значений, нарушающих бизнес-правила, 409 для конфликтов состояния, 412 для проваленных preconditions, 401 для «кто ты» против 403 для «не разрешено», с 404, иногда подставляемым вместо 403, чтобы не утечь факт существования ресурса. Коды 5xx распределяют вину — 500 твой баг, 502 плохой апстрим, 503 сознательная недоступность, которая должна нести Retry-After, 504 таймаут, где работа могла втайне завершиться. Ретрай 5xx и 429; никогда слепо не ретрай неидемпотентный 4xx или неидемпотентный запрос после таймаута, и берись за ключ идемпотентности, чтобы ретраи не списали дважды. Кардинальный грех — тоннелировать ошибку через тело 200: это ослепляет дашборды, отравляет кэши и глушит ретраи, которые спасли бы запрос.