Как делать12 октября 2026 г.7 мин чтения
Справочник разработчикаШаг 3 из 8

HTTP-коды ответа: справочник

Коротко

Код ответа — трёхзначное число, первая цифра которого задаёт класс: 1xx — промежуточный ответ, 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента, 5xx — ошибка сервера. По RFC 9110 клиент обязан понимать хотя бы класс и неизвестный код трактовать как x00 этого класса. В API чаще всего нужны 200, 201, 204, 304, 400, 401, 403, 404, 409, 422, 429, 500 и 503.

СправочникУровень: НачальныйHTTP-коды ответа

После материала вы сможете

  • назвать точное значение частых кодов 1xx–5xx по RFC 9110 и RFC 6585
  • выбрать код для ответа API и различать пары 401/403, 400/422, 404/410, 301/308
  • вернуть ошибку в формате application/problem+json (RFC 9457) и проверить заголовки ответа через curl -i

Проверено на

RFC 9110 (HTTP Semantics)RFC 6585RFC 9457 (Problem Details)Node.js 24 LTS (24.18, 24.20)curl 8.21, 8.22

Сверено с документацией: 12 октября 2026 г.

Содержание
  1. 01Классы кодов и общие правила
  2. 021xx и 2xx: промежуточные ответы и успех
  3. 033xx: перенаправления и 304
  4. 044xx: ошибки клиента
  5. 055xx: ошибки сервера
  6. 06Частые путаницы и шпаргалка для API
  7. 07Тело ошибки: Problem Details (RFC 9457)
  8. 08Проверка: сервер на node:http и curl -i

Классы кодов и общие правила

1xx

Смысл по RFC 9110
Информационный: запрос получен, обработка продолжается
Что делать клиенту
Ждать окончательный ответ; промежуточных может быть несколько

2xx

Смысл по RFC 9110
Успех: запрос получен, понят и принят
Что делать клиенту
Использовать ответ

3xx

Смысл по RFC 9110
Перенаправление: нужно дополнительное действие
Что делать клиенту
Перейти по Location или взять копию из кэша (304)

4xx

Смысл по RFC 9110
Ошибка клиента: неверный запрос или его нельзя выполнить
Что делать клиенту
Исправить запрос; повтор без изменений не поможет (кроме 408, 429)

5xx

Смысл по RFC 9110
Ошибка сервера: корректный запрос не выполнен
Что делать клиенту
Повторить позже, если операция идемпотентна
  • Допустимые коды — от 100 до 599. Если клиент не знает код, он обрабатывает его как x00 своего класса: незнакомый 471 — как 400.
  • Поясняющая фраза (Not Found, Created) — только рекомендация: её можно заменить или опустить. Программа должна смотреть на число.
  • На один запрос приходит ноль или больше ответов 1xx и ровно один окончательный ответ другого класса.
  • Некоторые коды кэшируются эвристически, без явных заголовков: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501.

1xx и 2xx: промежуточные ответы и успех

100 Continue

Значение
Начало запроса принято и не отклонено
Когда отдавать
Клиент прислал Expect: 100-continue перед большим телом; обычно делает сервер или фреймворк

101 Switching Protocols

Значение
Сервер переходит на протокол из заголовка Upgrade
Когда отдавать
Например, переход на WebSocket

103 Early Hints

Значение
Подсказка заголовков будущего ответа (RFC 8297)
Когда отдавать
Отдать Link: rel=preload, пока страница ещё собирается

200 OK

Значение
Запрос выполнен; содержимое зависит от метода
Когда отдавать
GET с данными, POST/PUT с результатом в теле

201 Created

Значение
Создан новый ресурс; его адрес — в Location или URI запроса
Когда отдавать
POST, создавший запись; тело обычно описывает созданное

202 Accepted

Значение
Принято в обработку, но не завершено — и может не выполниться
Когда отдавать
Фоновые задачи: отдайте адрес, где смотреть статус

204 No Content

Значение
Успех, тела нет и быть не может
Когда отдавать
DELETE, PUT/PATCH без возвращаемых данных

206 Partial Content

Значение
Отдана часть ресурса по заголовку Range
Когда отдавать
Докачка файлов, перемотка видео

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

301 Moved Permanently

Значение
Ресурс навсегда переехал, ссылки стоит обновить
Когда отдавать
Смена адреса страницы; для POST браузер может сменить метод на GET

302 Found

Значение
Ресурс временно по другому адресу; старый URI остаётся основным
Когда отдавать
Временный переезд для GET

303 See Other

Значение
Смотрите другой ресурс (GET по Location)
Когда отдавать
После POST-формы — на страницу результата (Post/Redirect/Get)

304 Not Modified

Значение
Условный GET/HEAD: у клиента актуальная копия
Когда отдавать
Запрос пришёл с If-None-Match / If-Modified-Since, и ресурс не менялся

307 Temporary Redirect

Значение
Временно по другому адресу; метод менять НЕЛЬЗЯ
Когда отдавать
Временный переезд API, где важно сохранить POST и тело

308 Permanent Redirect

Значение
Навсегда по другому адресу; метод менять нельзя
Когда отдавать
Постоянный переезд API: /v1/orders → /v2/orders

Навсегда

Метод может смениться на GET
301
Метод и тело сохраняются
308

Временно

Метод может смениться на GET
302
Метод и тело сохраняются
307
301, 302, 307, 308 — две оси выбора

4xx: ошибки клиента

400 Bad Request

Значение
Сервер не может или не будет обрабатывать запрос из-за ошибки клиента
Когда отдавать
Битый JSON, неверный синтаксис, невалидные заголовки

401 Unauthorized

Значение
Нет действующих учётных данных; обязателен WWW-Authenticate
Когда отдавать
Нет токена, токен истёк или неверен

403 Forbidden

Значение
Запрос понят, но сервер отказывается его выполнять
Когда отдавать
Пользователь известен, но прав не хватает; повтор с теми же данными бесполезен

404 Not Found

Значение
Ресурс не найден — или сервер не хочет раскрывать, что он есть
Когда отдавать
Нет записи; также вместо 403, чтобы скрыть существование

405 Method Not Allowed

Значение
Метод известен, но не поддерживается ресурсом; обязателен Allow
Когда отдавать
PUT на ресурс, где есть только GET и DELETE

406 Not Acceptable

Значение
Нет представления под Accept клиента
Когда отдавать
Клиент просит XML, а API отдаёт только JSON

408 Request Timeout

Значение
Сервер не дождался полного запроса
Когда отдавать
Обычно отдаёт сам сервер или прокси

409 Conflict

Значение
Конфликт с текущим состоянием ресурса; клиент может его разрешить
Когда отдавать
Дубликат уникального значения, параллельное изменение

410 Gone

Значение
Ресурс удалён, и это, скорее всего, навсегда
Когда отдавать
Снятая акция, удалённый аккаунт — когда сервер это точно знает

412 Precondition Failed

Значение
Не выполнено условие из заголовков (If-Match и др.)
Когда отдавать
Защита от потерянного обновления: ETag уже другой

413 Content Too Large

Значение
Тело больше, чем сервер готов принять
Когда отдавать
Слишком большой загружаемый файл

415 Unsupported Media Type

Значение
Формат тела не поддерживается
Когда отдавать
Прислали форму вместо application/json; можно указать Accept

422 Unprocessable Content

Значение
Тип и синтаксис тела верны, но обработать его нельзя
Когда отдавать
Ошибки валидации: пустое обязательное поле, дата в прошлом

428 Precondition Required

Значение
Сервер требует условный запрос (RFC 6585)
Когда отдавать
PUT без If-Match там, где важна защита от потерянного обновления

429 Too Many Requests

Значение
Слишком много запросов за период (RFC 6585); можно Retry-After
Когда отдавать
Превышен лимит; кэшировать такой ответ нельзя

431 Request Header Fields Too Large

Значение
Заголовки слишком большие — все или один (RFC 6585)
Когда отдавать
Раздутые cookie

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

500 Internal Server Error

Значение
Непредвиденная ситуация помешала выполнить запрос
Когда отдавать
Необработанное исключение; подробности — в лог, не клиенту

501 Not Implemented

Значение
Сервер не поддерживает нужную функциональность
Когда отдавать
Метод неизвестен серверу ни для одного ресурса

502 Bad Gateway

Значение
Шлюз или прокси получил некорректный ответ от сервера за ним
Когда отдавать
Nginx не достучался до приложения или получил мусор

503 Service Unavailable

Значение
Временно не может обработать: перегрузка или обслуживание; можно Retry-After
Когда отдавать
Плановые работы, отключение при перегрузке

504 Gateway Timeout

Значение
Шлюз или прокси не дождался ответа от сервера за ним
Когда отдавать
Приложение за прокси отвечает дольше таймаута

511 Network Authentication Required

Значение
Нужно авторизоваться в сети (RFC 6585)
Когда отдавать
Captive portal в Wi-Fi; в API не используется

Частые путаницы и шпаргалка для API

401 vs 403

Разница
401 — сервер не знает, кто вы (нет или неверные учётные данные); 403 — знает, но не пускает
Что вернуть
Нет токена → 401 с WWW-Authenticate; не та роль → 403

400 vs 422

Разница
400 — запрос не разобрать (битый JSON); 422 — разобран, но данные не проходят проверку
Что вернуть
Синтаксис → 400, бизнес-валидация → 422

404 vs 410

Разница
404 не говорит, временно ли отсутствие; 410 — сервер знает, что навсегда
Что вернуть
Не уверены → 404

404 vs 403

Разница
404 можно отдать вместо 403, чтобы не раскрывать, что ресурс существует
Что вернуть
Чужой заказ по прямой ссылке → часто 404

409 vs 412

Разница
409 — конфликт состояния; 412 — не выполнено явное условие из заголовка
Что вернуть
If-Match не совпал → 412

301/302 vs 307/308

Разница
301/302 допускают смену POST на GET, 307/308 — нет
Что вернуть
Для API — 308 или 307

500 vs 502/503/504

Разница
500 — сбой самого приложения; 502/504 — проблема между прокси и приложением; 503 — временная недоступность
Что вернуть
Смотрите, кто отдал ответ: приложение или прокси

200 с ошибкой в теле

Разница
Клиенты, кэши и мониторинг увидят успех
Что вернуть
Всегда отдавайте настоящий код ошибки

GET /items/1

Успех
200, 304 при совпавшем ETag
Типичные ошибки
404, 401, 403

POST /items

Успех
201 + Location; 202 для фоновой задачи
Типичные ошибки
400, 409, 415, 422, 429

PUT/PATCH /items/1

Успех
200 с ресурсом или 204
Типичные ошибки
404, 409, 412, 422

DELETE /items/1

Успех
204
Типичные ошибки
401, 403, 404

Метод не поддерживается

Успех
—
Типичные ошибки
405 + Allow
Какой код вернуть: типовые операции REST API

Тело ошибки: Problem Details (RFC 9457)

Код говорит, что случилось, а тело — почему. RFC 9457 задаёт единый JSON-формат с типом application/problem+json, поэтому клиент разбирает ошибки любого эндпоинта одинаково. Все поля необязательны; можно добавлять свои расширения, например errors со списком полей.

type

Что содержит
URI типа проблемы; без поля считается about:blank — «смысл только в HTTP-коде»

title

Что содержит
Короткое описание типа; для about:blank — как фраза кода (Not Found)

status

Что содержит
Код ответа числом — только для удобства; в настоящем ответе обязан быть тот же код

detail

Что содержит
Пояснение для этого случая, помогающее исправить запрос; клиенту не стоит его парсить

instance

Что содержит
URI конкретного случая ошибки
JSON
{
  "type": "about:blank",
  "title": "Unprocessable Content",
  "status": 422,
  "detail": "Field name is required",
  "errors": [
    {
      "pointer": "#/name",
      "detail": "must be a non-empty string"
    }
  ]
}
Тело реального ответа 422 учебного сервера ниже, отформатировано для чтения.

Проверка: сервер на node:http и curl -i

Учебный сервер без зависимостей отдаёт основные коды с правильными заголовками. Запустите node server.mjs (Node.js 24; если порт 3000 занят — PORT=3100 node server.mjs) и во втором терминале выполняйте curl -si: флаг -i показывает строку статуса и заголовки, -s прячет индикатор загрузки.

server.mjs
import { createServer } from "node:http";

const PORT = Number(process.env.PORT ?? 3000);
const items = new Map([[1, { id: 1, name: "coffee" }]]);
let nextId = 2;
const ETAG = '"v1"';
const hits = [];

function sendJson(res, status, data, headers = {}) {
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...headers });
  res.end(JSON.stringify(data));
}

// RFC 9457: errors as application/problem+json; "status" must equal the real status code
function problem(res, status, title, extra = {}, headers = {}) {
  res.writeHead(status, { "Content-Type": "application/problem+json", ...headers });
  res.end(JSON.stringify({ type: "about:blank", title, status, ...extra }));
}

async function readBody(req) {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

const server = createServer(async (req, res) => {
  const { pathname } = new URL(req.url, "http://localhost");

  try {
    if (pathname === "/old-page") {
      res.writeHead(301, { Location: "/new-page" });
      return res.end();
    }
    if (pathname === "/api/v1/orders") {
      res.writeHead(308, { Location: "/api/v2/orders" }); // method and body are preserved
      return res.end();
    }
    if (pathname === "/new-page" || pathname === "/api/v2/orders") {
      return sendJson(res, 200, { method: req.method, path: pathname });
    }
    if (pathname === "/promo-2024") return problem(res, 410, "Gone", { detail: "The promotion has ended" });
    if (pathname === "/maintenance") return problem(res, 503, "Service Unavailable", {}, { "Retry-After": "120" });
    if (pathname === "/crash") throw new Error("db connection lost");

    if (pathname === "/limited") {
      const now = Date.now();
      while (hits.length && now - hits[0] > 60_000) hits.shift();
      if (hits.length >= 2) return problem(res, 429, "Too Many Requests", {}, { "Retry-After": "60" });
      hits.push(now);
      return sendJson(res, 200, { ok: true });
    }

    if (pathname === "/items" && req.method === "POST") {
      if (!req.headers["content-type"]?.startsWith("application/json")) {
        return problem(res, 415, "Unsupported Media Type", {}, { Accept: "application/json" });
      }
      let data;
      try {
        data = JSON.parse(await readBody(req));
      } catch {
        return problem(res, 400, "Bad Request", { detail: "Request body is not valid JSON" });
      }
      if (typeof data?.name !== "string" || data.name.trim() === "") {
        return problem(res, 422, "Unprocessable Content", {
          detail: "Field name is required",
          errors: [{ pointer: "#/name", detail: "must be a non-empty string" }],
        });
      }
      if ([...items.values()].some((i) => i.name === data.name)) {
        return problem(res, 409, "Conflict", { detail: `Item "${data.name}" already exists` });
      }
      const item = { id: nextId++, name: data.name };
      items.set(item.id, item);
      return sendJson(res, 201, item, { Location: `/items/${item.id}` });
    }

    const match = pathname.match(/^\/items\/(\d+)$/);
    if (match) {
      const item = items.get(Number(match[1]));
      if (req.method === "GET") {
        if (!item) return problem(res, 404, "Not Found");
        const cache = { ETag: ETAG, "Cache-Control": "no-cache" };
        if (req.headers["if-none-match"] === ETAG) {
          res.writeHead(304, cache); // no body: the client reuses its cached copy
          return res.end();
        }
        return sendJson(res, 200, item, cache);
      }
      if (req.method === "DELETE") {
        const auth = req.headers.authorization;
        if (!auth) return problem(res, 401, "Unauthorized", {}, { "WWW-Authenticate": 'Bearer realm="api"' });
        if (auth !== "Bearer admin-token") return problem(res, 403, "Forbidden", { detail: "Admin role required" });
        if (!item) return problem(res, 404, "Not Found");
        items.delete(item.id);
        res.writeHead(204);
        return res.end();
      }
      return problem(res, 405, "Method Not Allowed", {}, { Allow: "GET, DELETE" });
    }

    return problem(res, 404, "Not Found");
  } catch (err) {
    console.error(err); // details go to the server log, not to the client
    return problem(res, 500, "Internal Server Error");
  }
});

server.listen(PORT, () => console.log(`API: http://localhost:${PORT}`));
Учебный пример, не production: токен admin-token — заглушка вместо настоящей проверки JWT или сессии, счётчик лимита общий на всех клиентов и живёт в памяти.
terminal
$ curl -si http://localhost:3000/items/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
ETag: "v1"
Cache-Control: no-cache
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"id":1,"name":"coffee"}

$ curl -si -H 'If-None-Match: "v1"' http://localhost:3000/items/1
HTTP/1.1 304 Not Modified
ETag: "v1"
Cache-Control: no-cache
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Первый запрос получает 200 и ETag; повторный с If-None-Match — 304 без тела. Node.js 24.20 + curl 8.22, реальный вывод.
terminal
$ curl -si -X POST http://localhost:3000/items -H 'Content-Type: application/json' -d '{"name":"tea"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /items/2
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"id":2,"name":"tea"}

$ curl -si -X POST http://localhost:3000/items -H 'Content-Type: application/json' -d '{"name":""}'
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Unprocessable Content","status":422,"detail":"Field name is required","errors":[{"pointer":"#/name","detail":"must be a non-empty string"}]}

$ curl -si -X DELETE http://localhost:3000/items/2
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer realm="api"
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Unauthorized","status":401}

$ curl -si -X PUT http://localhost:3000/items/1
HTTP/1.1 405 Method Not Allowed
Content-Type: application/problem+json
Allow: GET, DELETE
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Method Not Allowed","status":405}
201 с Location, 422 с Problem Details, 401 с WWW-Authenticate, 405 с Allow. Node.js пишет старую фразу Unprocessable Entity (из WebDAV, RFC 4918), RFC 9110 называет код Unprocessable Content — клиенту важно только число.
terminal
$ curl -si http://localhost:3000/old-page
HTTP/1.1 301 Moved Permanently
Location: /new-page
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

$ curl -sL -d 'x=1' http://localhost:3000/old-page
{"method":"GET","path":"/new-page"}

$ curl -sL -d 'x=1' http://localhost:3000/api/v1/orders
{"method":"POST","path":"/api/v2/orders"}

$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/limited
200

$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/limited
200

$ curl -si http://localhost:3000/limited
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Too Many Requests","status":429}

$ curl -si http://localhost:3000/crash
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
Date: Mon, 12 Oct 2026 09:21:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"type":"about:blank","title":"Internal Server Error","status":500}
При 301 curl с -L повторяет POST как GET, при 308 сохраняет метод. Третий запрос к /limited за минуту получает 429 и Retry-After; 500 не раскрывает текст исключения. Реальный вывод.

Как устроены запрос, заголовки и JSON — в материале «HTTP и JSON: запросы, ответы, заголовки и формат данных»; как выбрать коды, формат ошибок и пагинацию для целого API — в руководстве «Проектирование API: ресурсы, версии, ошибки, пагинация».

Как проверено2 · пройденосверено 12 октября 2026 г.
  • запущено, работает

    server.mjs (node:http, без зависимостей)

    node --check; tsc 5.9.3 --noEmit --allowJs --checkJs с @types/node 24.19 во временном проекте — без ошибок; запуск в docker node:24-alpine (Node 24.20) и локально на Windows (Node 24.18, PORT=3100, т. к. порт 3000 занят чужим процессом)

  • запущено, работает

    curl-сценарий curls.sh: 23 запроса (200, 304, 201, 409, 422, 400, 415, 401, 403, 204, 404, 405, 301, 308, повтор 301 и 308 с -L, 410, 200×2 и 429, 503, 500, 404)

    curl 8.22 в node:24-alpine и curl 8.21 на Windows: выводы совпали полностью, кроме заголовка Date и порта; блоки shellsession и json в статье собраны скриптом build.mjs из транскрипта docker-прогона без ручного набора (json — то же тело 422, отформатированное JSON.stringify)

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.