Коды ответов HTTP: что значат 200, 301, 400, 404 и 500

Веб Автор: Среда и версия: Замеры 31 августа 2026: curl 8.15.0 (OpenSSL 3.5.7) с koddo.ru и httpbin.org
содержание

Код ответа — это трёхзначное число, которым сервер начинает свой ответ ещё до тела. Он говорит не «что внутри», а «как читать то, что внутри»: удалось ли, надо ли идти в другое место, кто виноват, если не удалось.

Запоминать все сто с лишним кодов не нужно и вредно. Достаточно первой цифры: она делит их на пять классов, и класс сразу подсказывает, чей теперь ход — твой или сервера.

Веб / 01

Первая цифра показывает класс ответа

Первая цифра показывает класс ответакод смысл следующий шаг 1xx информация обработка продолжается 2xx успех запрос выполнен 3xx редирект / кэш проверить точный код 4xx ошибка клиента проверить запрос 5xx ошибка сервера разобрать сбойкодсмыслследующий шаг1xxинформацияобработка продолжается2xxуспехзапрос выполнен3xxредирект / кэшпроверить точный код4xxошибка клиентапроверить запрос5xxошибка сервераразобрать сбой
Первая цифра отвечает на единственный вопрос: чей теперь ход. Один класс не определяет политику повторов: например, при 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 запрещают менять метод при автоматическом переходе.

Веб / 02

Редирект может изменить метод и потерять тело

Редирект может изменить метод и потерять тело01 301 / 302 / 303 POST /form → GET без тела Для 301/302 клиент может сменить POST на GET; 303 ведёт к получению ресурса. 02 307 / 308 POST /form → POST с a=1 Метод и тело сохраняются при повторном запросе к цели.01301 / 302 / 303POST /form→ GET без телаДля 301/302 клиент может сменить POST наGET; 303 ведёт к получению ресурса.02307 / 308POST /form→ POST с a=1Метод и тело сохраняются при повторномзапросе к цели.
Если форма «молча не сохраняется» на проде и работает локально, посмотри цепочку редиректов: 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 байт
Веб / 03

ETag решает, нужно ли передавать тело заново

ETag решает, нужно ли передавать тело заново01 ETag совпал 304 0 байт тела If-None-Match совпал с текущей версией. Используем копию из кэша. 02 ETag изменился 200 59 637 байт В сохранённом примере сервер передал актуальное тело страницы.01ETag совпал3040 байт телаIf-None-Match совпал с текущей версией.Используем копию из кэша.02ETag изменился20059 637 байтВ сохранённом примере сервер передалактуальное тело страницы.
Один и тот же URL, одна и та же страница: разница между 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

  1. Получилось и есть что показать — 200Получилось, но показывать нечего (удаление, сохранение) — 204 и пустое тело. Создали новый объект — 201 и его адрес в Location.
  2. Не разобрал запрос — 400Битый JSON, отсутствует обязательное поле, строка вместо числа. Клиент не сможет исправиться, пока не изменит сам запрос.
  3. Разобрал, но значения не годятся — 422Email без собаки, дата в прошлом, отрицательное количество. Отделять от 400 стоит, если у клиента разная реакция: показать «повторите» или подсветить поле формы.
  4. Нужны учётные данные — 401. Запрос запрещён — 403Отсутствие подходящей аутентификации и отказ по политике доступа — разные причины. Код 403 сам по себе не подтверждает, что пользователь вошёл.
  5. Состояние не позволяет — 409Такой email уже занят, версия документа устарела, заказ уже оплачен. Запрос правильный, но мир изменился — клиент должен перечитать и решить заново.
  6. Упал сам — 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' <адрес>. Полный путь запроса, частью которого они являются, разобран в статье что происходит, когда вводишь адрес сайта.

Источники