Частые ошибки с методами и статусами

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

Антипаттерн REST — устойчивое неправильное решение, которое «вроде работает», но нарушает контракт HTTP и потому ломает клиентов, кэши, мониторинг или безопасность.

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

200 на всё, даже на ошибку

Самый распространённый антипаттерн: сервер всегда отвечает 200 OK, а реальный исход прячет в теле.

HTTP/1.1 200 OK
{
  "success": false,
  "error": "order not found"
}

Почему это плохо? HTTP-клиент видит 200 и считает запрос удавшимся: fetch ставит response.ok = true, raise_for_status() молчит, обёртки не кидают исключений. Каждый потребитель API обязан вручную лезть в тело и разбирать поле success — и стоит одному забыть, ошибка проглатывается молча. Кэши тоже сбиты с толку: они закэшируют «ошибку» как валидный ответ. Правильно — отдать код, отражающий исход: 404, если ресурса нет, и держать тело для деталей.

HTTP/1.1 404 Not Found
{
  "error": "order_not_found",
  "order_id": 999
}

404 vs 403: раскрытие существования

Эта пара — про безопасность. Допустим, пользователь запрашивает чужой приватный ресурс /users/777/private-notes, к которому у него нет доступа. Какой код вернуть?

  • 403 Forbidden честно говорит: «ресурс есть, но тебе нельзя». Но этим вы подтверждаете факт существования ресурса. Перебирая id, атакующий по разнице 403/404 вычислит, какие записи существуют (enumeration).
  • 404 Not Found на запрещённый ресурс скрывает сам факт его существования — снаружи «нет доступа» и «нет ресурса» неотличимы.

Для чувствительных ресурсов (приватные данные, чужие аккаунты) безопаснее отдавать 404 вместо 403, чтобы не сливать информацию о структуре данных. Для внутренних API, где enumeration не угроза, 403 честнее и понятнее. Выбор — осознанный, по модели угроз.

401 vs 403: ты кто и что тебе можно

Эти коды путают чаще всего, хотя смысл у них разный — про разные этапы:

КодВопросКогдаЧто делать клиенту
401 Unauthorized«Кто ты?»токена нет, истёк или неверен — личность не установленавойти / обновить токен
403 Forbidden«Что тебе можно?»личность ясна, но прав на действие нетповторный вход не поможет

Практическая разница в реакции клиента. На 401 разумно отправить пользователя на логин или молча обновить токен и повторить. На 403 повторная авторизация бесполезна — нужно показать «недостаточно прав». Перепутав их, вы отправите в бесконечный цикл логина пользователя, которому просто не хватает роли.

Неверный 500 на ошибку клиента

Частый источник ложных тревог. Код принимает qty, конвертирует в число и падает, если пришла строка — а исключение улетает наружу как 500.

Запрос:  POST /orders  {"qty": "много"}
Ответ:   500 Internal Server Error   ← НЕВЕРНО, это вина клиента

500 означает «сломались мы», поднимает алерт и будит дежурного. Но виноват клиент — прислал не то. Правильно провалидировать ввод до опасной операции и вернуть 400/422. Эмпирика: если причина ошибки — данные запроса, это 4xx; 500 оставляйте для настоящих багов сервера.

POST там, где нужен PUT

Когда у ресурса уже есть известный клиенту URL и его надо обновить целиком, верный метод — PUT (идемпотентный), а не POST.

# Антипаттерн: POST для обновления известного ресурса
curl -X POST https://api.shop.test/users/12/profile -d '{...}'

# Верно: PUT — идемпотентно, ретрай безопасен
curl -X PUT  https://api.shop.test/users/12/profile -d '{...}'

Цена ошибки — потеря идемпотентности: POST нельзя безопасно повторить, и при сетевом сбое ретрай может натворить дел. Используя PUT, вы получаете бесплатные безопасные ретраи. POST оставляйте для создания (URL назначает сервер) и для операций вне CRUD.

Тело в ответе на 204

204 No Content по контракту означает «успех, и тела нет». Некоторые серверы всё равно дописывают JSON в ответ 204 — это нарушение: часть клиентов и прокси, увидев 204, даже не читают тело, и данные теряются. Если вам есть что вернуть (например, обновлённый ресурс после PATCH) — берите 200 OK с телом. 204 — только когда возвращать действительно нечего.

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

Почему эти ошибки так дорого обходятся? Потому что HTTP-инфраструктура принимает код статуса на веру и действует автоматически, не заглядывая в тело:

Ответ 200 + {"error":true}
   -> клиент:  response.ok = true   (ошибку проглотил)
   -> кэш:     закэширую как успех  (раздаёт ошибку другим)

Ответ 500 на чужую опечатку
   -> мониторинг:  +1 к error rate  (будит дежурного)
   -> балансировщик: ретрай          (бьёт по серверу зря)

Ответ 403 на приватный ресурс
   -> атакующий: "ага, он существует" (enumeration)

Код статуса — это сигнал не одному клиенту, а всей цепочке: браузеру, кэшу, прокси, балансировщику, мониторингу, поисковику. Соврав в коде, вы вводите в заблуждение их всех разом. Поэтому «мелкая» неточность в статусе оборачивается то ложным алертом в 3 ночи, то утечкой структуры данных.

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

  • Универсальный 200: прячете исход в теле — ломаете обработку ошибок и кэширование у всех клиентов.
  • 403 на приватный ресурс: подтверждаете существование записи; для чувствительных данных безопаснее 404.
  • Путать 401 и 403: отправляете на повторный логин того, кому не хватает прав, — бесконечный цикл.
  • 500 на невалидный ввод: ложные алерты и зряшные ретраи; валидируйте и отдавайте 400/422.
  • POST вместо PUT для обновления известного URL — теряете безопасные ретраи.
  • Тело в 204: данные потеряются; нужно вернуть содержимое — берите 200.

Итог

  • Никогда не прячьте ошибку под 200 — код статуса должен отражать реальный исход.
  • 404 вместо 403 на чувствительных ресурсах скрывает их существование (защита от enumeration).
  • 401 — «не аутентифицирован» (войди), 403 — «нет прав» (повторный вход не поможет).
  • Ошибка из-за данных клиента — это 4xx, не 500; для обновления известного URL берите идемпотентный PUT; у 204 тела не бывает.
Проверьте себя
1. API на запрос несуществующего заказа отвечает 200 OK с телом {"success": false, "error": "not found"}. Чем это плохо?
AНичем, это удобно для клиента
BКлиенты и кэши считают ответ успешным; ошибка проглатывается, нужен код 404
CНарушается формат JSON
DТело слишком большое
2. Пользователь запросил чужой приватный ресурс без прав доступа. Почему для чувствительных данных часто возвращают 404, а не 403?
A404 быстрее обрабатывается
B403 запрещён стандартом
C403 подтверждает существование ресурса и помогает атакующему перебором (enumeration), а 404 скрывает сам факт его наличия
D404 не требует аутентификации
3. Чем 401 отличается от 403?
A401 — нет прав на действие, 403 — не аутентифицирован
B401 — личность не установлена (войди/обнови токен), 403 — личность ясна, но прав нет (повторный вход не поможет)
CЭто синонимы
D401 для GET, 403 для POST
4. Клиент прислал {"qty": "много"}, и сервер упал с 500 при конвертации в число. Как правильно?
AОставить 500 — любая ошибка это 500
BВернуть 200 с пустым телом
CПровалидировать ввод и вернуть 400/422, ведь виноват клиент, а не сервер
DВернуть 403