Logo
Overview

HTTP коды ответов: полный справочник для разработчика и аналитика

May 5, 2026
11 min read

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 на ошибку. Мониторинг спокоен, графики зелёные, а пользователи пишут в поддержку.

Пять групп кодов: общая картина

Все коды делятся на пять групп по первой цифре. Это удобно: даже не зная конкретного значения, по группе понятно, на чьей стороне произошло событие.

ГруппаДиапазонСмыслКто «виноват»
1xx100–199ИнформационныйНикто, идёт обмен метаданными
2xx200–299УспехВсё хорошо
3xx300–399ПеренаправлениеКлиенту нужно сходить ещё куда-то
4xx400–499Ошибка клиентаКлиент прислал что-то не то
5xx500–599Ошибка сервераСервер сам упал или не справился

Запомнить просто: чётные сотни — норма, нечётные — куда-то надо идти. Четыре — сам дурак, пять — мы дураки.

100%
flowchart TD
  REQ["HTTP-запрос от клиента"]
  SRV["Сервер обработал запрос"]
  REQ --> SRV
  SRV --> G1["1xx Информационный"]
  SRV --> G2["2xx Успех"]
  SRV --> G3["3xx Перенаправление"]
  SRV --> G4["4xx Ошибка клиента"]
  SRV --> G5["5xx Ошибка сервера"]

  G1 --> C100["100 Continue"]
  G1 --> C101["101 Switching Protocols"]

  G2 --> C200["200 OK"]
  G2 --> C201["201 Created"]
  G2 --> C204["204 No Content"]

  G3 --> C301["301 Moved Permanently"]
  G3 --> C302["302 Found"]
  G3 --> C304["304 Not Modified"]

  G4 --> C400["400 Bad Request"]
  G4 --> C401["401 Unauthorized"]
  G4 --> C403["403 Forbidden"]
  G4 --> C404["404 Not Found"]
  G4 --> C409["409 Conflict"]
  G4 --> C422["422 Unprocessable Entity"]
  G4 --> C429["429 Too Many Requests"]

  G5 --> C500["500 Internal Server Error"]
  G5 --> C502["502 Bad Gateway"]
  G5 --> C503["503 Service Unavailable"]
  G5 --> C504["504 Gateway Timeout"]

  style REQ fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style SRV fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style G1 fill:#e0e0e0,stroke:#999,color:#000
  style G2 fill:#50c878,stroke:#3a9a5c,color:#fff
  style G3 fill:#f0a500,stroke:#c88400,color:#fff
  style G4 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style G5 fill:#d9534f,stroke:#a83232,color:#fff
  style C200 fill:#50c878,stroke:#3a9a5c,color:#fff
  style C201 fill:#50c878,stroke:#3a9a5c,color:#fff
  style C204 fill:#50c878,stroke:#3a9a5c,color:#fff
  style C400 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C401 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C403 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C404 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C409 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C422 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C429 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style C500 fill:#d9534f,stroke:#a83232,color:#fff
  style C502 fill:#d9534f,stroke:#a83232,color:#fff
  style C503 fill:#d9534f,stroke:#a83232,color:#fff
  style C504 fill:#d9534f,stroke:#a83232,color:#fff
  style C100 fill:#e0e0e0,stroke:#999,color:#000
  style C101 fill:#e0e0e0,stroke:#999,color:#000
  style C301 fill:#f0a500,stroke:#c88400,color:#fff
  style C302 fill:#f0a500,stroke:#c88400,color:#fff
  style C304 fill:#f0a500,stroke:#c88400,color:#fff

На диаграмме видно дерево решений сервера: после обработки запроса он выбирает одну из пяти веток, и уже внутри ветки — конкретный код. Дальше пробежимся по каждой группе и разберём те коды, которые встречаются в реальной жизни, а не только в 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/42204. Пустое тело без Content-Length: 0 — норма.

206 Partial Content

Сервер отдал часть ресурса, потому что клиент попросил через заголовок Range. Так работают докачки больших файлов и стриминг видео.

Правило: если сомневаетесь между 200 и 204 — выбирайте по тому, есть ли что положить в тело. Если есть — 200. Если бы вы клали туда пустой объект — лучше 204.

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

Сервер говорит клиенту: «то, что ты ищешь, лежит вон там, сходи». Главная путаница здесь — между 301 и 302.

КодНазваниеПостоянное?Кэшируется?Когда применять
301Moved PermanentlyДаАгрессивноСтарый URL ушёл навсегда
302FoundНетНет (по умолчанию)Временное перенаправление
303See OtherНетНетПосле POST направить на GET-страницу
304Not ModifiedУсловный запрос, ресурс не менялся
307Temporary RedirectНетНетКак 302, но метод не меняется
308Permanent 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° по Фаренгейту». Программисты любят пасхалки.

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

Здесь команда облажалась. Эти коды должны попадать в мониторинг и поднимать алерты.

КодНазваниеЧто случилось
500Internal Server ErrorЧто-то упало внутри. Универсальный «всё плохо»
501Not ImplementedМетод не реализован. Реально редкий код
502Bad GatewayПрокси/балансировщик не дождался ответа от апстрима
503Service UnavailableСервис временно недоступен (перегрузка, деплой)
504Gateway TimeoutТаймаут от апстрима через прокси
507Insufficient StorageКончилось место на диске

Принципиальное различие: 500 — упал сам сервис. 502/504 — упал не наш сервис, а тот, к кому мы ходим. 503 — мы живы, но сейчас не работаем (это часто отдают во время деплоя или maintenance).

Важно: 5xx ошибки не должны зависеть от данных запроса. Если один и тот же запрос то падает в 500, то нет — это баг. Если 500 падает на любой запрос с определённым полем — это 400 или 422, замаскированный под 500.

Как выбирать код: дерево решений

Чтобы каждый раз не гадать, держите перед глазами схему. Она покрывает 90% реальных случаев.

100%
flowchart TD
  START["Сервер получил запрос"]
  Q1{"Запрос корректно распарсился?"}
  Q2{"Клиент аутентифицирован?"}
  Q3{"У клиента есть права?"}
  Q4{"Ресурс существует?"}
  Q5{"Метод разрешён для этого URL?"}
  Q6{"Бизнес-валидация прошла?"}
  Q7{"Конфликт с текущим состоянием?"}
  Q8{"Превышен rate limit?"}
  Q9{"Сервер смог обработать?"}
  Q10{"Это создание нового ресурса?"}
  Q11{"Есть что вернуть в теле?"}

  R400["400 Bad Request"]
  R401["401 Unauthorized"]
  R403["403 Forbidden"]
  R404["404 Not Found"]
  R405["405 Method Not Allowed"]
  R422["422 Unprocessable Entity"]
  R409["409 Conflict"]
  R429["429 Too Many Requests"]
  R500["500 Internal Server Error"]
  R201["201 Created"]
  R200["200 OK"]
  R204["204 No Content"]

  START --> Q1
  Q1 -->|"Нет"| R400
  Q1 -->|"Да"| Q2
  Q2 -->|"Нет"| R401
  Q2 -->|"Да"| Q3
  Q3 -->|"Нет"| R403
  Q3 -->|"Да"| Q4
  Q4 -->|"Нет"| R404
  Q4 -->|"Да"| Q5
  Q5 -->|"Нет"| R405
  Q5 -->|"Да"| Q6
  Q6 -->|"Нет"| R422
  Q6 -->|"Да"| Q7
  Q7 -->|"Да"| R409
  Q7 -->|"Нет"| Q8
  Q8 -->|"Да"| R429
  Q8 -->|"Нет"| Q9
  Q9 -->|"Нет"| R500
  Q9 -->|"Да"| Q10
  Q10 -->|"Да"| R201
  Q10 -->|"Нет"| Q11
  Q11 -->|"Да"| R200
  Q11 -->|"Нет"| R204

  style START fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style Q1 fill:#f0a500,stroke:#c88400,color:#fff
  style Q2 fill:#f0a500,stroke:#c88400,color:#fff
  style Q3 fill:#f0a500,stroke:#c88400,color:#fff
  style Q4 fill:#f0a500,stroke:#c88400,color:#fff
  style Q5 fill:#f0a500,stroke:#c88400,color:#fff
  style Q6 fill:#f0a500,stroke:#c88400,color:#fff
  style Q7 fill:#f0a500,stroke:#c88400,color:#fff
  style Q8 fill:#f0a500,stroke:#c88400,color:#fff
  style Q9 fill:#f0a500,stroke:#c88400,color:#fff
  style Q10 fill:#f0a500,stroke:#c88400,color:#fff
  style Q11 fill:#f0a500,stroke:#c88400,color:#fff
  style R400 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R401 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R403 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R404 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R405 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R422 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R409 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R429 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style R500 fill:#d9534f,stroke:#a83232,color:#fff
  style R201 fill:#50c878,stroke:#3a9a5c,color:#fff
  style R200 fill:#50c878,stroke:#3a9a5c,color:#fff
  style R204 fill:#50c878,stroke:#3a9a5c,color:#fff

Это не догма, это ориентир. В реальной жизни иногда выбор между 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. Если вы возвращаете его на продакшене, чайник в команде, скорее всего, не один.