Коды статуса: 2xx/3xx/4xx/5xx

Урок про то, как код ответа сообщает клиенту исход запроса — и как выбрать правильный код вместо вечного 200.

Код статуса HTTP — трёхзначное число в ответе, которое сообщает клиенту исход запроса машиночитаемо: удалось ли, кто виноват при ошибке и что делать дальше.

Код статуса — это первое, что читает клиент, ещё до тела ответа. По нему HTTP-библиотеки решают: бросить исключение или нет, ретраить или нет, идти по редиректу или нет. Если ваш API на любой исход отвечает 200 OK и прячет реальный результат в JSON-поле {"error": true}, вы заставляете каждого клиента вручную разбирать тело и ломаете всю автоматику HTTP. Правильный код — это контракт, понятный без чтения документации.

Четыре класса кодов

Первая цифра задаёт класс — общий смысл ответа:

КлассСмыслКто «виноват»
2xxУспех — запрос принят и обработан
3xxПеренаправление — нужен ещё шаг
4xxОшибка клиента — запрос неверенклиент
5xxОшибка сервера — сервер не справилсясервер

Граница между 4xx и 5xx — важнейшая. 4xx значит «исправь запрос, повторять как есть бесполезно». 5xx значит «с твоим запросом всё ок, проблема у нас — возможно, поможет ретрай». От этого зависят алертинг (5xx будят дежурного, 4xx обычно нет) и поведение клиента.

2xx — успех

  • 200 OK — универсальный успех с телом-ответом. Подходит для GET, удачного PUT/PATCH, POST-операций, возвращающих результат.
  • 201 Created — создан новый ресурс. Сопровождается заголовком Location с его адресом. Типичный ответ на POST, создавший сущность.
  • 202 Accepted — запрос принят, но обработка ещё идёт (асинхронно). Используют для очередей и долгих задач: «приняли, проверь статус позже».
  • 204 No Content — успех без тела. Идеален для DELETE и для PUT/PATCH, когда возвращать нечего. Тела у 204 быть не должно.
HTTP/1.1 201 Created
Location: /orders/43

HTTP/1.1 204 No Content
(пустое тело)

3xx — перенаправления

  • 301 Moved Permanently — ресурс навсегда переехал на новый URL (в заголовке Location). Клиенты и поисковики обновляют ссылку.
  • 302 Found — временное перенаправление; исходный URL остаётся каноничным.
  • 304 Not Modified — кэш-ответ. На условный GET с If-None-Match/If-Modified-Since сервер говорит «не изменилось, бери из кэша» и не шлёт тело, экономя трафик.
curl -i https://api.shop.test/orders/42 \
  -H 'If-None-Match: "v7"'   # -> 304 Not Modified, тела нет

4xx — ошибки клиента

КодКогда
400 Bad Requestтело/синтаксис запроса битые: невалидный JSON, отсутствует обязательное поле
401 Unauthorizedне аутентифицирован: токена нет, он истёк или неверен
403 Forbiddenаутентифицирован, но нет прав на это действие
404 Not Foundресурса по этому URL не существует
405 Method Not Allowedметод не поддержан для ресурса; нужен заголовок Allow
409 Conflictконфликт с текущим состоянием: дубль уникального поля, конкурентное изменение
422 Unprocessable Entityсинтаксис верен, но данные не проходят бизнес-валидацию (email не email, qty < 0)
429 Too Many Requestsпревышен лимит запросов (rate limit); часто с заголовком Retry-After

Тонкая, но частая пара — 400 против 422. 400 — запрос невозможно даже разобрать (сломанный JSON). 422 — разобрать удалось, поля на месте, но значения не проходят правила предметной области. Многие API упрощают и всё валидационное отдают как 400 — это допустимо, но 422 точнее.

{
  "error": "validation_failed",
  "details": [
    {"field": "qty", "message": "должно быть больше нуля"},
    {"field": "email", "message": "некорректный адрес"}
  ]
}

5xx — ошибки сервера

  • 500 Internal Server Error — необработанное исключение, баг на сервере. Тело отдаём общее, без стектрейсов наружу.
  • 502 Bad Gateway — шлюз/прокси получил неверный ответ от вышестоящего сервиса.
  • 503 Service Unavailable — сервис временно недоступен (перегрузка, деплой, обслуживание). Часто с Retry-After: «вернись через N секунд».

Как работает под капотом

Код статуса — это число в стартовой строке ответа, сразу за версией протокола:

HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 41

{"error": "order 999 not found"}

Текст после числа («Not Found») — пояснение для людей, машина смотрит только на число. HTTP-клиенты опираются на класс: requests в Python кидает исключение по raise_for_status() для 4xx/5xx; браузерный fetch ставит response.ok = false при коде вне 2xx; редиректы 3xx многие клиенты проходят автоматически. Вот почему правильный код — это не косметика: на нём построено поведение всей экосистемы.

Полезные коды-спутники несут заголовки: 201Location, 429/503Retry-After, 405Allow. Они превращают код из простого «да/нет» в инструкцию к действию.

Частые ошибки

  • Тело в 204: по контракту 204 No Content не имеет тела. Если хотите вернуть данные — берите 200.
  • 201 без Location: создали ресурс, но не сказали где он. Клиенту приходится гадать URL.
  • 200 на ошибку: «успех» с {"error":...} внутри ломает обработку ошибок у клиента (подробно — в следующем уроке).
  • 500 на ошибку клиента: невалидный ввод — это 400/422, а не 500. Иначе вы будите дежурного из-за чужой опечатки.

Итог

  • Класс кода — это смысл: 2xx успех, 3xx редирект, 4xx вина клиента, 5xx вина сервера.
  • 201 идёт с Location; 204 — успех без тела; 202 — принято, обработка асинхронна.
  • 401 — «кто ты?», 403 — «тебе нельзя»; 400 — битый запрос, 422 — невалидные данные; 409 — конфликт состояния; 429 — лимит.
  • 5xx сигналит о проблеме сервера и может оправдывать ретрай; 4xx требует исправить запрос.
Проверьте себя
1. Клиент прислал синтаксически верный JSON, но qty = -3, что запрещено бизнес-правилами. Какой код точнее всего?
A500 Internal Server Error
B404 Not Found
C422 Unprocessable Entity
D204 No Content
2. После успешного DELETE /orders/42 сервер не возвращает тело. Какой код корректен?
A204 No Content
B200 OK с обязательным телом
C201 Created
D304 Not Modified
3. Невалидный ввод от клиента (битый JSON) сервер вернул как 500. В чём проблема?
AНи в чём, 500 подходит для любых ошибок
B500 означает вину сервера и поднимет ложный алерт, тогда как виноват клиент — нужен 400
CНужно было вернуть 200 с описанием ошибки
DНужно было вернуть 301
4. Что означает 304 Not Modified в ответ на условный GET с If-None-Match?
AРесурс удалён
BРесурс не изменился — используй кэшированную копию, тело не отправлено
CУ клиента нет прав на ресурс
DСервер временно недоступен