Код ответа — трёхзначное число, первая цифра которого задаёт класс: 1xx — промежуточный ответ, 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента, 5xx — ошибка сервера. По RFC 9110 клиент обязан понимать хотя бы класс и неизвестный код трактовать как x00 этого класса. В API чаще всего нужны 200, 201, 204, 304, 400, 401, 403, 404, 409, 422, 429, 500 и 503.
После материала вы сможете
- назвать точное значение частых кодов 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 г.
Содержание
Классы кодов и общие правила
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
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
Тело ошибки: 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 конкретного случая ошибки
{
"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 прячет индикатор загрузки.
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}`));admin-token — заглушка вместо настоящей проверки JWT или сессии, счётчик лимита общий на всех клиентов и живёт в памяти.$ 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=5200 и ETag; повторный с If-None-Match — 304 без тела. Node.js 24.20 + curl 8.22, реальный вывод.$ 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 — клиенту важно только число.$ 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.