Код ответа — это трёхзначное число, которым сервер начинает свой ответ ещё до тела. Он говорит не «что внутри», а «как читать то, что внутри»: удалось ли, надо ли идти в другое место, кто виноват, если не удалось.
Запоминать все сто с лишним кодов не нужно и вредно. Достаточно первой цифры: она делит их на пять классов, и класс сразу подсказывает, чей теперь ход — твой или сервера.
Первая цифра показывает класс ответа
429 полезна пауза, а 5xx не гарантирует, что сервер ещё не выполнил операцию.Что видно на живом сайте
Три запроса к одному домену дают три разных класса. Вывод ниже снят 31 августа 2026, curl 8.15.0:
curl -s -o /dev/null -w 'code=%{http_code} redirect=%{redirect_url}\n' https://koddo.ru/
curl -s -o /dev/null -w 'code=%{http_code} redirect=%{redirect_url}\n' https://www.koddo.ru/
curl -s -o /dev/null -w 'code=%{http_code} redirect=%{redirect_url}\n' http://koddo.ru/
curl -s -o /dev/null -w 'code=%{http_code}\n' https://koddo.ru/no-such-page-xyz
code=200 redirect=
code=301 redirect=https://koddo.ru/
code=308 redirect=https://koddo.ru/
code=404
Здесь уже видна деталь, мимо которой проходят: один и тот же сайт отдаёт 301 на www и 308 на http. Оба означают «переехали навсегда», но 308 дополнительно обещает, что метод запроса не изменится. Почему это важно — ниже.
2xx: сервер сделал, что просили
| Код | Когда возвращают | Есть ли тело |
|---|---|---|
200 OK | Обычный успешный ответ на GET, PUT, PATCH | Да |
201 Created | Создан новый ресурс; в Location — его адрес | Обычно да |
202 Accepted | Задача принята в очередь, результата ещё нет | Часто ссылка на статус |
204 No Content | Успех, но показывать нечего: DELETE, сохранение формы | Нет, и не должно быть |
206 Partial Content | Отдана часть файла по заголовку Range — докачка, перемотка видео | Да, кусок |
Две ошибки встречаются постоянно. Первая — 200 с текстом ошибки внутри: клиент видит успех, парсит тело и падает уже на нём. Вторая — 204 с непустым телом: по RFC 9110 тела там быть не может, и часть прокси его просто отрежет.
3xx: ресурс живёт в другом месте
Класс 3xx почти целиком про редиректы, и внутри него есть разделение, которое ломает POST-запросы, если про него не знать. Проверим на живом сервере: отправим форму и посмотрим, каким методом запрос придёт после перехода.
for c in 301 302 303 307 308; do
curl -s -L -d 'a=1' "https://httpbin.org/redirect-to?url=/anything&status_code=$c" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['method'], d['form'])"
done
GET {}
GET {}
GET {}
POST {'a': '1'}
POST {'a': '1'}
В этом прогоне curl после 301, 302 и 303 отправил GET без тела, а после 307 и 308 сохранил POST и тело. Спецификация разрешает клиенту менять POST на GET при 301 и 302, но не требует этого от любой библиотеки. 303 перенаправляет к получению другого ресурса через GET или HEAD; 307 и 308 запрещают менять метод при автоматическом переходе.
Редирект может изменить метод и потерять тело
301 с http на https может привести к потере тела POST. Лечится заменой на 308 либо тем, что клиент сразу ходит по https.Ловушка при проверке: флаг -X POST ломает этот эксперимент. Он принудительно назначает метод каждому запросу в цепочке, и 301 перестаёт выглядеть виновным:
curl -s -L -X POST -d 'a=1' 'https://httpbin.org/redirect-to?url=/anything&status_code=301'
# метод в ответе: POST — в отличие от прогона curl без -X выше
Проверяй редиректы через -d, а не через -X, иначе воспроизведёшь не тот баг, который ищешь.
304 Not Modified: ответ, у которого нет тела
304 стоит особняком: это не перенаправление, а «у тебя уже есть свежая копия». Сервер отдаёт заголовки и ноль байт тела. Разница измерима:
ET=$(curl -s -D - -o /dev/null https://koddo.ru/ | grep -i '^etag' | tr -d '\r' | cut -d' ' -f2)
curl -s -o /dev/null -w 'с If-None-Match: код %{http_code}, скачано %{size_download} байт\n' -H "If-None-Match: $ET" https://koddo.ru/
curl -s -o /dev/null -w 'без If-None-Match: код %{http_code}, скачано %{size_download} байт\n' https://koddo.ru/
с If-None-Match: код 304, скачано 0 байт
без If-None-Match: код 200, скачано 59637 байт
ETag решает, нужно ли передавать тело заново
304 и 200 здесь — 59 637 байт на каждый повторный визит. Именно поэтому ETag и Last-Modified выставляют даже на маленьких сайтах.4xx: действие зависит от конкретного статуса
Самый населённый класс, и самый неправильно используемый.
| Код | Что означает буквально | Частая путаница |
|---|---|---|
400 Bad Request | Запрос синтаксически сломан: битый JSON, неверный формат параметра | Возвращают на любую ошибку вообще, включая бизнес-правила |
401 Unauthorized | Ты не представился или токен просрочен | Название врёт: это про аутентификацию, а не про права |
403 Forbidden | Сервер понял запрос, но отказывается его выполнять | Отдают вместо 401, и клиент не понимает, надо ли перелогиниться |
404 Not Found | Ресурса по этому адресу нет | Отдают вместо 403, чтобы скрыть само существование ресурса — это осознанный приём, а не ошибка |
405 Method Not Allowed | Адрес есть, но не для этого метода | Забывают обязательный заголовок Allow |
409 Conflict | Запрос противоречит текущему состоянию: дубликат, устаревшая версия | Заменяют на 400, и клиент не может отличить «почини запрос» от «перечитай и повтори» |
422 Unprocessable Content | Синтаксис верный, значения не проходят валидацию | Спор 400 против 422 вечен; главное — быть последовательным внутри одного API |
429 Too Many Requests | Превышен лимит частоты | Не кладут Retry-After, и клиент долбится дальше вслепую |
401 означает отсутствие подходящих учётных данных, 403 — отказ выполнять понятный серверу запрос. При 403 пользователь не обязательно опознан: причина может быть в политике доступа или фильтре запросов. На 401 проверь авторизацию, а на 403 — причину отказа; бесконечно повторять запрос с теми же данными не нужно.
Оба сопровождаются заголовками, и их видно:
curl -s -D - -o /dev/null https://httpbin.org/status/401 | grep -iE '^(HTTP|www-authenticate)'
curl -s -D - -o /dev/null -X POST https://httpbin.org/get | grep -iE '^(HTTP|allow)'
HTTP/2 401
www-authenticate: Basic realm="Fake Realm"
HTTP/2 405
allow: GET, HEAD, OPTIONS
405 без Allow — распространённый недочёт: клиент узнал, что метод не тот, но не узнал, какой нужен. У 429 та же история с Retry-After, и его действительно часто нет — в проверке выше httpbin ответил 429 вообще без этого заголовка. Спецификация RFC 6585 заголовок рекомендует, но не требует, поэтому клиент обязан иметь собственную выдержку с нарастающей паузой.
5xx: сервер не справился
| Код | Что произошло | Кто чинит |
|---|---|---|
500 Internal Server Error | Необработанное исключение в коде | Владелец сервера; в логах есть трассировка |
501 Not Implemented | Метод сервером не поддерживается вообще | Владелец сервера |
502 Bad Gateway | Прокси сходил к приложению и получил мусор или ничего | Проверить прокси, его upstream и приложение |
503 Service Unavailable | Временно недоступен: перегрузка, деплой, обслуживание | Ждать; часто есть Retry-After |
504 Gateway Timeout | Прокси не дождался ответа приложения | Медленный запрос за прокси или слишком короткий таймаут |
502 и 504 сообщают о сбое при работе шлюза с вышестоящим сервером. Проверь и приложение, и прокси: его логи помогают отличить отказ соединения, неверный upstream, ошибку TLS и таймаут. Например, nginx позволяет записывать адрес upstream и время соединения или получения заголовков.
При временном сбое повтор может помочь, но сначала проверь, допускает ли операция безопасное повторение. Для идемпотентных запросов используй ограниченное число попыток и нарастающую паузу со случайным разбросом; учитывай Retry-After, если он есть. Не повторяй любой 5xx автоматически: например, 501 обычно требует изменить запрос или сервер.
Код 0: ответа не было вообще
Отдельный случай, который путают с 5xx. Если запрос вовсе не дошёл или ответ не удалось прочитать, HTTP-кода нет. curl показывает 000:
curl -s -m 8 -o /dev/null -w 'code=%{http_code}\n' https://httpstat.us/503
code=000
Это не «сервер ответил нулём»: curl не получил HTTP-статус. В браузере отсутствие доступного ответа может выглядеть как TypeError: Failed to fetch, но CORS способен скрыть даже полученный HTTP-ответ. Возможны сбои DNS, TLS, сети и ограничения браузера; curl не применяет CORS и CSP страницы. Разбор всех вариантов — в статье про ошибку Failed to fetch; отдельно про запрет браузером — в разборе ошибки CORS policy.
Практическое следствие: «нет доступного ответа» и «код 500» — разные ветки диагностики, но ни одна не доказывает, что операция не выполнена. Сервер мог изменить данные, а ответ — потеряться или оказаться недоступным из-за CORS. Повторяй только идемпотентную операцию, запрос с поддерживаемой сервером защитой от дублирования либо запрос, про который достоверно известно, что он не был применён.
Какой код возвращать в своём API
- Получилось и есть что показать — 200Получилось, но показывать нечего (удаление, сохранение) —
204и пустое тело. Создали новый объект —201и его адрес вLocation. - Не разобрал запрос — 400Битый JSON, отсутствует обязательное поле, строка вместо числа. Клиент не сможет исправиться, пока не изменит сам запрос.
- Разобрал, но значения не годятся — 422Email без собаки, дата в прошлом, отрицательное количество. Отделять от
400стоит, если у клиента разная реакция: показать «повторите» или подсветить поле формы. - Нужны учётные данные — 401. Запрос запрещён — 403Отсутствие подходящей аутентификации и отказ по политике доступа — разные причины. Код 403 сам по себе не подтверждает, что пользователь вошёл.
- Состояние не позволяет — 409Такой email уже занят, версия документа устарела, заказ уже оплачен. Запрос правильный, но мир изменился — клиент должен перечитать и решить заново.
- Упал сам — 500, и ни строчкой подробностей наружуТрассировка, SQL-запрос и путь к файлу в теле ответа — это подарок атакующему. Наружу идёт идентификатор ошибки, детали остаются в логе.
Отдельно про то, чего делать не надо: не возвращай 200 с полем "error" внутри. Такой ответ ломает всё, что стоит между клиентом и сервером — кэши считают его валидным и сохраняют, мониторинг рисует стопроцентную доступность, а клиентская библиотека не бросает исключение и уходит парсить несуществующие данные.
Частые вопросы
Чем 401 отличается от 403
401 означает, что для ресурса не хватает подходящих учётных данных: например, токен отсутствует или просрочен. 403 — сервер понял запрос, но отказал; это возможно и без аутентификации. При 401 проверь или обнови учётные данные, при 403 выясни причину запрета, не зацикливая обновление токена.
Что значит код ответа 0
HTTP-статуса 0 не существует. 000 у curl означает, что он не получил статус ответа. В браузере Response.status === 0 бывает у непрозрачного ответа (no-cors), а Failed to fetch — это отклонение промиса без доступного Response. Ни один из этих сигналов сам по себе не доказывает, что сервер не выполнил запрос.
Чем 301 отличается от 302 и 308
301 и 308 — «переехали навсегда», их кэшируют и запоминают. 302 — «временно, продолжай ходить сюда». Разница между 301 и 308 — в методе: 301 разрешает превратить POST в GET, 308 это запрещает. Для переезда сайта на https корректнее 308, если по адресу приходят не только GET-запросы.
Почему на 404 всё-таки приходит HTML-страница
Код и тело независимы. 404 описывает исход, а тело может содержать что угодно, включая красиво свёрстанную страницу с поиском по сайту. Ошибка — не наличие тела, а обратное: отдать эту страницу с кодом 200, из-за чего поисковик проиндексирует несуществующий адрес как рабочий.
Какой код у успешного удаления
204, если возвращать нечего, или 200 с телом, если клиенту полезен итог операции. 202 подойдёт, когда удаление уходит в очередь и на момент ответа ещё не произошло.
Где потренироваться
Коды видны в панели Network любого браузера — вкладка Status. В терминале быстрее всего curl -s -o /dev/null -w '%{http_code}\n' <адрес>. Полный путь запроса, частью которого они являются, разобран в статье что происходит, когда вводишь адрес сайта.