A status code is a three-digit number whose first digit sets the class: 1xx interim response, 2xx success, 3xx redirection, 4xx client error, 5xx server error. Per RFC 9110 a client must understand at least the class and treat an unknown code as the x00 of that class. APIs mostly need 200, 201, 204, 304, 400, 401, 403, 404, 409, 422, 429, 500 and 503.
After this you will be able to
- state the exact meaning of common 1xx–5xx codes per RFC 9110 and RFC 6585
- pick a status code for an API response and tell apart 401/403, 400/422, 404/410, 301/308
- return errors as
application/problem+json(RFC 9457) and check response headers withcurl -i
Know beforehand
Verified with
RFC 9110 (HTTP Semantics)RFC 6585RFC 9457 (Problem Details)Node.js 24 LTS (24.18, 24.20)curl 8.21, 8.22
Checked against docs: October 12, 2026
Contents
Status code classes and general rules
1xx
- Meaning per RFC 9110
- Informational: request received, processing continues
- What the client does
- Wait for the final response; there may be several interim ones
2xx
- Meaning per RFC 9110
- Successful: request received, understood and accepted
- What the client does
- Use the response
3xx
- Meaning per RFC 9110
- Redirection: further action is needed
- What the client does
- Follow
Locationor use the cached copy (304)
4xx
- Meaning per RFC 9110
- Client error: bad request or it cannot be fulfilled
- What the client does
- Fix the request; repeating it unchanged will not help (except 408, 429)
5xx
- Meaning per RFC 9110
- Server error: a valid request was not fulfilled
- What the client does
- Retry later if the operation is idempotent
- Valid codes are 100 to 599. A client that does not know a code treats it as the x00 of its class: an unfamiliar
471is handled as400. - The reason phrase (
Not Found,Created) is only a recommendation and may be replaced or omitted. Software should look at the number. - A request gets zero or more 1xx responses followed by exactly one final response of another class.
- Some codes are heuristically cacheable without explicit headers:
200,203,204,206,300,301,308,404,405,410,414,501.
1xx and 2xx: interim responses and success
100 Continue
- Meaning
- The start of the request was received and not rejected
- When to send it
- The client sent
Expect: 100-continuebefore a large body; usually handled by the server or framework
101 Switching Protocols
- Meaning
- The server switches to the protocol named in
Upgrade - When to send it
- For example, switching to WebSocket
103 Early Hints
- Meaning
- Hints about headers of the coming response (RFC 8297)
- When to send it
- Send
Link: rel=preloadwhile the page is still being built
200 OK
- Meaning
- The request succeeded; the content depends on the method
- When to send it
GETwith data,POST/PUTwith a result in the body
201 Created
- Meaning
- A new resource was created; its URI is in
Locationor the request URI - When to send it
- A
POSTthat created a record; the body usually describes it
202 Accepted
- Meaning
- Accepted for processing but not finished — and it may never be
- When to send it
- Background jobs: return a URI where the status can be checked
204 No Content
- Meaning
- Success, with no body and none allowed
- When to send it
DELETE,PUT/PATCHwith nothing to return
206 Partial Content
- Meaning
- Part of the resource, per the
Rangeheader - When to send it
- Resumable downloads, video seeking
3xx: redirects and 304
301 Moved Permanently
- Meaning
- The resource moved for good; references should be updated
- When to send it
- A page changed its URL; for
POSTa browser may switch toGET
302 Found
- Meaning
- Temporarily at another URI; the old URI stays canonical
- When to send it
- A temporary move for
GET
303 See Other
- Meaning
- See another resource (
GETitsLocation) - When to send it
- After a form
POST, to the result page (Post/Redirect/Get)
304 Not Modified
- Meaning
- Conditional
GET/HEAD: the client's copy is still valid - When to send it
- The request had
If-None-Match/If-Modified-Sinceand nothing changed
307 Temporary Redirect
- Meaning
- Temporarily elsewhere; the method MUST NOT change
- When to send it
- A temporary API move where
POSTand its body must be kept
308 Permanent Redirect
- Meaning
- Permanently elsewhere; the method must not change
- When to send it
- A permanent API move:
/v1/orders→/v2/orders
Permanent
- Method may change to GET
301- Method and body preserved
308
Temporary
- Method may change to GET
302- Method and body preserved
307
4xx: client errors
400 Bad Request
- Meaning
- The server cannot or will not process the request due to a client error
- When to send it
- Broken JSON, bad syntax, invalid headers
401 Unauthorized
- Meaning
- No valid credentials;
WWW-Authenticateis required - When to send it
- No token, or the token is expired or invalid
403 Forbidden
- Meaning
- The request is understood but the server refuses it
- When to send it
- The user is known but lacks permission; retrying with the same credentials is pointless
404 Not Found
- Meaning
- No current representation — or the server will not disclose that one exists
- When to send it
- No such record; also instead of
403to hide existence
405 Method Not Allowed
- Meaning
- The method is known but not supported by the resource;
Allowis required - When to send it
PUTon a resource that only hasGETandDELETE
406 Not Acceptable
- Meaning
- No representation matches the client's
Accept - When to send it
- The client asks for XML, the API only serves JSON
408 Request Timeout
- Meaning
- The server did not receive a complete request in time
- When to send it
- Usually sent by the server or proxy itself
409 Conflict
- Meaning
- Conflict with the resource's current state; the client may resolve it
- When to send it
- Duplicate unique value, concurrent modification
410 Gone
- Meaning
- The resource is gone, likely for good
- When to send it
- An ended promotion, a deleted account — when the server knows for sure
412 Precondition Failed
- Meaning
- A condition in the headers (
If-Matchetc.) evaluated to false - When to send it
- Lost update protection: the
ETaghas changed
413 Content Too Large
- Meaning
- The body is larger than the server will accept
- When to send it
- An upload that is too big
415 Unsupported Media Type
- Meaning
- The body format is not supported
- When to send it
- Form data sent instead of
application/json;Acceptmay list what works
422 Unprocessable Content
- Meaning
- Type and syntax are fine, but the content cannot be processed
- When to send it
- Validation errors: empty required field, a date in the past
428 Precondition Required
- Meaning
- The server requires a conditional request (RFC 6585)
- When to send it
PUTwithoutIf-Matchwhere lost updates matter
429 Too Many Requests
- Meaning
- Too many requests in a period (RFC 6585);
Retry-Afteris allowed - When to send it
- Rate limit exceeded; the response must not be cached
431 Request Header Fields Too Large
- Meaning
- Headers are too large — overall or a single one (RFC 6585)
- When to send it
- Bloated cookies
5xx: server errors
500 Internal Server Error
- Meaning
- An unexpected condition prevented fulfilling the request
- When to send it
- An unhandled exception; details go to the log, not the client
501 Not Implemented
- Meaning
- The server does not support the required functionality
- When to send it
- The method is unknown to the server for any resource
502 Bad Gateway
- Meaning
- A gateway or proxy got an invalid response from the server behind it
- When to send it
- Nginx could not reach the app or got garbage back
503 Service Unavailable
- Meaning
- Temporarily unable to handle the request: overload or maintenance;
Retry-Afterallowed - When to send it
- Planned maintenance, load shedding
504 Gateway Timeout
- Meaning
- A gateway or proxy did not get a timely response from the server behind it
- When to send it
- The app behind the proxy takes longer than the timeout
511 Network Authentication Required
- Meaning
- The client must authenticate to get network access (RFC 6585)
- When to send it
- Wi-Fi captive portals; not used in APIs
Common mix-ups and an API cheat sheet
401 vs 403
- Difference
401— the server does not know who you are (missing or invalid credentials);403— it knows and still says no- What to return
- No token →
401withWWW-Authenticate; wrong role →403
400 vs 422
- Difference
400— the request cannot be parsed (broken JSON);422— parsed, but the data fails validation- What to return
- Syntax →
400, business validation →422
404 vs 410
- Difference
404says nothing about permanence;410— the server knows it is permanent- What to return
- Not sure →
404
404 vs 403
- Difference
404may replace403to avoid revealing that a resource exists- What to return
- Someone else's order by direct link → often
404
409 vs 412
- Difference
409— state conflict;412— an explicit header condition failed- What to return
If-Matchmismatch →412
301/302 vs 307/308
- Difference
301/302allowPOSTto becomeGET,307/308do not- What to return
- For APIs —
308or307
500 vs 502/503/504
- Difference
500— the app itself failed;502/504— trouble between proxy and app;503— temporarily unavailable- What to return
- Check who produced the response: the app or the proxy
200 with an error in the body
- Difference
- Clients, caches and monitoring see a success
- What to return
- Always send the real error code
GET /items/1
- Success
200,304on a matchingETag- Typical errors
404,401,403
POST /items
- Success
201+Location;202for a background job- Typical errors
400,409,415,422,429
PUT/PATCH /items/1
- Success
200with the resource, or204- Typical errors
404,409,412,422
DELETE /items/1
- Success
204- Typical errors
401,403,404
Unsupported method
- Success
- —
- Typical errors
405+Allow
Error bodies: Problem Details (RFC 9457)
The code says what happened; the body says why. RFC 9457 defines one JSON format with the application/problem+json media type, so a client parses errors from every endpoint the same way. All members are optional, and you can add extensions such as an errors list of fields.
type
- Contents
- URI of the problem type; when absent it is
about:blank— “no meaning beyond the HTTP code”
title
- Contents
- Short summary of the type; for
about:blank, the status phrase (Not Found)
status
- Contents
- The status code as a number — advisory only; the actual response must use the same code
detail
- Contents
- Explanation of this occurrence that helps fix the request; clients should not parse it
instance
- Contents
- URI of this specific occurrence
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "Field name is required",
"errors": [
{
"pointer": "#/name",
"detail": "must be a non-empty string"
}
]
}422 response from the server below, formatted for reading.Check it: a node:http server and curl -i
This dependency-free server returns the key codes with the right headers. Run node server.mjs (Node.js 24; if port 3000 is taken, PORT=3100 node server.mjs) and use curl -si in a second terminal: -i prints the status line and headers, -s hides the progress meter.
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 is a stub in place of real JWT or session checks, and the rate-limit counter is shared by all clients and kept in memory.$ 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 and an ETag; the repeat with If-None-Match gets a body-less 304. Node.js 24.20 + curl 8.22, real output.$ 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 with Location, 422 with Problem Details, 401 with WWW-Authenticate, 405 with Allow. Node.js still prints the old phrase Unprocessable Entity (from WebDAV, RFC 4918) while RFC 9110 names the code Unprocessable Content — only the number matters to clients.$ 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 with -L repeats the POST as a GET; on 308 it keeps the method. The third /limited request within a minute gets 429 and Retry-After; the 500 does not leak the exception text. Real output.How requests, headers and JSON work is covered in “HTTP and JSON: requests, responses, headers and data format”; choosing codes, error formats and pagination for a whole API is in “API design: resources, versioning, errors, pagination”.
How this was verified2 · passedchecked 12 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.