HTTP коды ответов — это не «магические цифры». Это договор между клиентом и сервером, который держит всё API на плаву.
Если вы когда-нибудь получали в ответ от чужого API 200 OK с телом {"error": "user not found"} — поздравляю, вы видели, как разработчики не договорились. Один считает, что любая HTTP-ошибка означает «упал сервер», другой — что коды нужны только если всё совсем плохо. Третий слышал про 418 I'm a teapot и теперь возвращает его на любой бизнес-конфликт, потому что «прикольно».
Давайте разберёмся системно. Что означают группы кодов, чем 400 отличается от 422, когда уместен 409, и почему 401 и 403 — это вообще про разные вещи.
Этот пост — расширение статьи про основные принципы REST API и логичное дополнение к правилам именования endpoint’ов. Если вы их ещё не читали — самое время.
Зачем вообще нужны HTTP статус коды
HTTP status code — трёхзначное число в первой строке ответа сервера, которое описывает результат обработки запроса. Это часть протокола HTTP, а не вашей бизнес-логики.
Коды решают две задачи. Первая — машиночитаемость. Прокси, балансировщики, CDN, мониторинг, retry-логика клиента — всё это смотрит на код ответа и принимает решение: кэшировать, повторять, гасить алерт, перенаправлять. Если вы возвращаете 200 OK на ошибку, вы лишаете всю эту инфраструктуру возможности работать.
Вторая задача — единый язык. Разработчик мобильного клиента, фронтендер, ребята из соседнего сервиса, тестировщик — у всех разный код, разный стек, иногда разные часовые пояса. HTTP-код — то, на чём они сходятся без переводчика.
На своём примере могу сказать: половина продакшен-инцидентов, где «ничего не понятно, но что-то не работает», начинается именно с того, что сервис возвращает
200на ошибку. Мониторинг спокоен, графики зелёные, а пользователи пишут в поддержку.
Пять групп кодов: общая картина
Все коды делятся на пять групп по первой цифре. Это удобно: даже не зная конкретного значения, по группе понятно, на чьей стороне произошло событие.
| Группа | Диапазон | Смысл | Кто «виноват» |
|---|---|---|---|
| 1xx | 100–199 | Информационный | Никто, идёт обмен метаданными |
| 2xx | 200–299 | Успех | Всё хорошо |
| 3xx | 300–399 | Перенаправление | Клиенту нужно сходить ещё куда-то |
| 4xx | 400–499 | Ошибка клиента | Клиент прислал что-то не то |
| 5xx | 500–599 | Ошибка сервера | Сервер сам упал или не справился |
Запомнить просто: чётные сотни — норма, нечётные — куда-то надо идти. Четыре — сам дурак, пять — мы дураки.
На диаграмме видно дерево решений сервера: после обработки запроса он выбирает одну из пяти веток, и уже внутри ветки — конкретный код. Дальше пробежимся по каждой группе и разберём те коды, которые встречаются в реальной жизни, а не только в RFC.
1xx: информационные коды
Группа, про которую большинство разработчиков знает в формате «ну есть такая, и ладно». И это нормально — в 99% веб-API эти коды вы не увидите.
- 100 Continue — клиент отправил заголовки и спрашивает «можно слать тело?», сервер отвечает «можно».
- 101 Switching Protocols — переключение на WebSocket или HTTP/2. Если вы открывали вкладку Network в DevTools и видели
101рядом сwss://— это оно. - 103 Early Hints — относительно новый код, сервер заранее присылает заголовки, чтобы браузер начал подгружать ресурсы.
Примечательно, что эти коды чаще всего обрабатывает не ваш код, а сам HTTP-клиент или прокси. Возвращать их вручную из бизнес-логики не нужно.
2xx: всё хорошо
Здесь начинается то, что встречается каждый день.
200 OK
Универсальный код успеха. Запрос обработан, тело ответа содержит результат. Подходит для GET, PUT, PATCH, DELETE (если возвращается тело).
201 Created
Ресурс создан. Возвращается на POST, который привёл к появлению нового объекта. Хорошая практика — добавить заголовок Location: /orders/42, указывающий на созданный ресурс.
202 Accepted
Запрос принят, но обработка ещё не закончилась. Используется для асинхронных операций: загрузили видео, началась транскодировка, клиенту отдали 202 и ссылку на статус.
204 No Content
Операция выполнена, но возвращать в теле нечего. Классика: DELETE /orders/42 → 204. Пустое тело без Content-Length: 0 — норма.
206 Partial Content
Сервер отдал часть ресурса, потому что клиент попросил через заголовок Range. Так работают докачки больших файлов и стриминг видео.
Правило: если сомневаетесь между
200и204— выбирайте по тому, есть ли что положить в тело. Если есть —200. Если бы вы клали туда пустой объект — лучше204.
3xx: перенаправления
Сервер говорит клиенту: «то, что ты ищешь, лежит вон там, сходи». Главная путаница здесь — между 301 и 302.
| Код | Название | Постоянное? | Кэшируется? | Когда применять |
|---|---|---|---|---|
301 | Moved Permanently | Да | Агрессивно | Старый URL ушёл навсегда |
302 | Found | Нет | Нет (по умолчанию) | Временное перенаправление |
303 | See Other | Нет | Нет | После POST направить на GET-страницу |
304 | Not Modified | — | — | Условный запрос, ресурс не менялся |
307 | Temporary Redirect | Нет | Нет | Как 302, но метод не меняется |
308 | Permanent Redirect | Да | Да | Как 301, но метод не меняется |
Главная ловушка: на 301 и 302 старые клиенты могли менять POST на GET после редиректа. Из-за этого появились 307 и 308 — они гарантируют, что метод сохранится.
304 Not Modified — отдельная история. Это ответ на условный запрос с заголовком If-None-Match или If-Modified-Since. Сервер говорит: «у тебя уже свежая версия в кэше, я тебе тело не пришлю». Полезно для экономии трафика.
4xx: ошибка клиента
Самая интересная и самая «холиварная» группа. Здесь чаще всего ошибаются.
400 Bad Request
Запрос невалиден на уровне формата: битый JSON, отсутствует обязательный заголовок, неверный тип параметра. То есть сервер даже не смог распарсить то, что прислали.
401 Unauthorized
Клиент не идентифицирован. Несмотря на название «unauthorized» (не авторизован), по смыслу это «не аутентифицирован» — мы не знаем, кто ты. Возвращается, когда нет токена, токен просрочен, токен невалиден. Подробнее про это — в статье про OAuth и JWT.
403 Forbidden
Клиент идентифицирован, но прав на эту операцию у него нет. То есть «я знаю, кто ты, но тебе сюда нельзя». Классика: пользователь залогинен, но пытается удалить чужой заказ.
Простая проверка: если клиенту нужно «перелогиниться, чтобы получить доступ» — это
401. Если «никакой логин не поможет, прав всё равно нет» — это403.
404 Not Found
Ресурс не существует. И вот тут начинается тонкость: 404 отличается от 403 тем, что мы говорим «такого вообще нет», а не «есть, но не для тебя». Иногда из соображений безопасности сервер намеренно возвращает 404 вместо 403, чтобы не подтверждать существование ресурса.
405 Method Not Allowed
URL правильный, но метод не поддерживается. Послал DELETE на /api/health — получил 405. Хорошая практика — добавить заголовок Allow: GET, POST со списком разрешённых методов.
409 Conflict
Запрос конфликтует с текущим состоянием ресурса. Примеры:
- Регистрация пользователя с уже существующим email.
- Обновление документа, который кто-то уже изменил (оптимистическая блокировка).
- Попытка удалить заказ, у которого уже есть оплата.
410 Gone
Ресурс существовал, но был навсегда удалён. Отличается от 404 тем, что сервер уверенно говорит «было, но нет». Используется редко, но в API с публичными ссылками — полезно.
415 Unsupported Media Type
Клиент прислал тело в формате, который сервер не понимает. Например, отправили XML туда, где ждут JSON.
422 Unprocessable Entity
Самый недооценённый код. JSON распарсился (значит, не 400), формат правильный (значит, не 415), но содержимое не проходит бизнес-валидацию. Например, в поле email пришла строка без @, или quantity = -5.
На моей практике 80% случаев, когда команды используют
400— это на самом деле422. Разделение помогает:400— «JSON битый»,422— «JSON-то нормальный, но данные внутри плохие».
429 Too Many Requests
Превышен rate limit. Хорошая практика — отдавать заголовок Retry-After: 60, чтобы клиент знал, сколько ждать перед повтором.
451 Unavailable For Legal Reasons
Контент скрыт по решению суда или регулятора. Номер кода — отсылка к роману Брэдбери «451° по Фаренгейту». Программисты любят пасхалки.
5xx: ошибка сервера
Здесь команда облажалась. Эти коды должны попадать в мониторинг и поднимать алерты.
| Код | Название | Что случилось |
|---|---|---|
500 | Internal Server Error | Что-то упало внутри. Универсальный «всё плохо» |
501 | Not Implemented | Метод не реализован. Реально редкий код |
502 | Bad Gateway | Прокси/балансировщик не дождался ответа от апстрима |
503 | Service Unavailable | Сервис временно недоступен (перегрузка, деплой) |
504 | Gateway Timeout | Таймаут от апстрима через прокси |
507 | Insufficient Storage | Кончилось место на диске |
Принципиальное различие: 500 — упал сам сервис. 502/504 — упал не наш сервис, а тот, к кому мы ходим. 503 — мы живы, но сейчас не работаем (это часто отдают во время деплоя или maintenance).
Важно: 5xx ошибки не должны зависеть от данных запроса. Если один и тот же запрос то падает в 500, то нет — это баг. Если 500 падает на любой запрос с определённым полем — это 400 или 422, замаскированный под 500.
Как выбирать код: дерево решений
Чтобы каждый раз не гадать, держите перед глазами схему. Она покрывает 90% реальных случаев.
Это не догма, это ориентир. В реальной жизни иногда выбор между 403 и 404 зависит от модели угроз, а между 409 и 422 — от того, как вы трактуете «конфликт». Но для большинства endpoint’ов эта схема даёт корректный ответ.
Типичные ошибки при работе с кодами
Что встречается на код-ревью чаще всего.
Ошибка 1: возврат 200 OK с полем error внутри. Так делают, потому что «чтобы фронт не падал». Это анти-паттерн: вы ломаете контракт HTTP. Мониторинг будет считать всё хорошим, retry-логика — не будет повторять, кэш закэширует ошибку.
Ошибка 2: всё валится в 500. Ситуация: пользователь прислал email без собачки, сервис упал в исключение, клиент получил 500. Вместо этого должен быть 422 с описанием проблемы. Простой тест: если виноват клиент — это 4xx.
Ошибка 3: путаница 401 и 403. Возвращают 403, когда нужно 401, и наоборот. Из-за этого клиенты не знают: перезапрашивать токен или показывать «нет доступа».
Ошибка 4: 404 на endpoint, который существует. Если URL есть, но метод не поддерживается — это 405. Если URL принципиально нет — 404. Разница важна для генерации SDK и автодокументации.
Ошибка 5: 204 с непустым телом. По спецификации 204 No Content означает буквально пустое тело. Если вы кладёте туда JSON — некоторые клиенты это просто отбросят.
Что должно быть в теле ответа при ошибке
Сам код — это половина истории. Вторая половина — структурированное тело с деталями. Хорошая практика — стандарт RFC 7807 (Problem Details for HTTP APIs):
{ "type": "https://api.example.com/errors/validation", "title": "Validation Failed", "status": 422, "detail": "Field 'email' must contain '@' symbol", "instance": "/orders", "errors": [ { "field": "email", "message": "Invalid format" }, { "field": "quantity", "message": "Must be positive" } ]}Что важно:
- Не дублируйте код в теле — он уже в HTTP-ответе.
- Используйте устойчивые ключи (
type,code), которые клиент сможет сравнивать программно. - Не выкладывайте стектрейсы наружу — это утечка инфраструктуры.
- Локализуйте
detail, если у вас международная аудитория, — но вtypeвсегда оставляйте машиночитаемый идентификатор.
Заключение
HTTP коды — это маленький, но удивительно мощный инструмент. Правильно подобранный код экономит часы дебага, а неправильный — добавляет страницы документации и недоумения в Slack-чате.
Главное правило простое: код описывает результат на уровне протокола, тело — детали на уровне бизнес-логики. Если у вас не получается выбрать между двумя кодами — посмотрите, что важнее для клиента: повторить запрос, перелогиниться, починить данные или сходить в поддержку. Ответ на этот вопрос обычно подсказывает правильную цифру.
PS. И помните: 418 I'm a teapot — это шутка из RFC 2324. Если вы возвращаете его на продакшене, чайник в команде, скорее всего, не один.