Частые ошибки с методами и статусами
Урок-разбор реальных антипаттернов: что ломается, когда методы и коды статуса используют неправильно, и как делать верно.
Антипаттерн 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тела не бывает.