Status codes

HTTP & the web · Essentials · 10 min

Clients, caches, monitoring and search engines all act on the status code before they look at the body.

The three-digit status code summarises the outcome; the first digit gives the class.

2xx success:

  • 200 OK, 201 Created (with a Location header pointing at the new resource), 202 Accepted (queued for later processing), 204 No Content (success, empty body).

3xx redirection:

  • 301 Moved Permanently and 308 Permanent Redirect; 302 Found and 307 Temporary Redirect. The difference between the pairs is that 307 and 308 guarantee the method and body are kept, while clients may turn a redirected POST into a GET after 301/302.
  • 303 See Other: go GET this other URL, the classic redirect after a form POST.
  • 304 Not Modified: your cached copy is still valid.

4xx client errors (the request needs fixing):

  • 400 Bad Request, 401 Unauthorized (actually "unauthenticated": no or bad credentials), 403 Forbidden (authenticated but not allowed), 404 Not Found, 405 Method Not Allowed, 409 Conflict, 410 Gone, 415 Unsupported Media Type, 422 Unprocessable Content (well-formed but invalid data), 429 Too Many Requests.

5xx server errors (the server failed):

  • 500 Internal Server Error, 502 Bad Gateway and 504 Gateway Timeout (a proxy couldn't get a good or timely answer from upstream), 503 Service Unavailable (overloaded or in maintenance, often with Retry-After).

Choose precisely: clients retry 503 and 429, never 400; monitoring alerts on 5xx; search engines drop pages that return 404 or 410. Returning 200 with an error message in the body breaks all of that.

Example

import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.url === "/old") { res.writeHead(301, { Location: "/new" }); return res.end(); }
  if (req.url === "/new") { res.writeHead(200, { "Content-Type": "text/plain" }); return res.end("hi"); }
  if (req.url === "/admin") { res.writeHead(403); return res.end(); }
  res.writeHead(404, { "Content-Type": "text/plain" }); res.end("not found");
});

server.listen(0, async () => {
  const base = `http://localhost:${server.address().port}`;
  for (const p of ["/old", "/admin", "/nope"]) {
    const r = await fetch(base + p, { redirect: "manual" });
    console.log(p, r.status, r.headers.get("location"));
  }
  // /old 301 /new
  // /admin 403 null
  // /nope 404 null
  server.close();
});

Remember

  • 2xx success, 3xx redirect, 4xx client error, 5xx server error
  • 401 = not authenticated, 403 = authenticated but not allowed
  • 307/308 keep the method; 303 switches to GET
  • Never return 200 for an error

Watch out for

  • Returning 200 with {"error": ...} in the body, which hides failures from clients, caches and monitoring.

Check yourself

  1. A logged-in user requests another user's private invoice. Which status fits best?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status