Skip to content

Status codes and the error contract (RFC 9457 Problem Details)

The status line tells every tool what to do; the problem body tells your own client which error it was.

Predict first

Before you read: predict

A video service's player fetches GET /titles/{id}/licence before playback, through ky 2.1.0 with ky's default retry settings. While its licence store is down, the endpoint answers 200 with {"success":false,"error":"licence store unavailable"}. The endpoint's on-call alert pages when more than 5% of its responses in a window are errors, as the API gateway's access log counts them. The store is down while 20 viewers press Play, once each.

What happens?

Pick one

The 30-second version

The status code is for software that never reads your body: client libraries retry by it, gateways and monitors count by it, balancers route by it. Pick it for what that software must do: retry, back off, re-authenticate, or stop. Then a problem details body (RFC 9457) tells your own client which error, and what to show.

The status line tells every tool what to do; the problem body tells your own client which error it was.

How it works

Step 1 of 14

The status line is read by software that never opens your body: client libraries, proxies, caches, load balancers, gateways and monitors.
In the kitchen
  • the printed boxes (all well; unwell, collect now) that the parents' app, the pick-up rota and the manager's board act on without reading anything else → the status code, read by generic software (steps 1–4)

Analogy: A day nursery's daily sheet

Each child's daily sheet has a few printed tick boxes, and a key worker's handwritten note underneath.

At the nursery In the system
the printed boxes (all well; unwell, collect now) that the parents' app, the pick-up rota and the manager's board act on without reading anything else the status code, read by generic software (steps 1–4)
the handwritten note under the boxes, for the parent who reads it the problem details body (step 13)
"all well" ticked, with "39° temperature, please collect" written underneath: no alert, and the parent comes at six as usual 200 with an error in the body (step 5)
"unwell, collect now" ticked for a grazed knee: every parent runs and the board shows an outbreak a server error for the caller's own mistake (step 6)
"settling-in visits are full this morning; come at two" 429 with Retry-After (step 12)
"Friday's room is full; ask again if a place frees" against a complete form asking for a six-month-old in the pre-school room against a half-filled form 409 against 422 against 400 (steps 7–9)
a stranger at the door asking about a named child hears "I can't tell you whether that child is here", the same words whether or not the child attends 404 instead of 403 (step 11)

In real terms: before choosing a status, ask what a client library, the gateway's alert and the balancer will do with it, because none of them reads your body.

Breaks down when: a parent can phone the key worker and ask; generic software never reads the note.

Contrasting case: a reply your own client reads in full, such as a validation error listing each field. There the body carries the detail, but the status line still decides what every tool in between does.

Examples, run for real

4.1 ExamHall: a submission after the timer ends

EXH-3640: when the timer ended at 10:30, the candidate API's closed-attempt check threw, so a late PUT /attempts/a-7/submission got 500. Another candidate's attempt got 403; a missing one got 404. MR !3940 answers a late submit with 409 and a problem body whose type names the closed attempt, and answers "not yours" exactly as "missing".

4.1 at 10:30 the timer ends; 30 candidates submit attempt a-7 late, once each (ky 2.1.0 defaults, PUT)
  as shipped (500)   requests 90 · 5xx alert pages 1 · app shows "Something went wrong"
  MR !3940 (409)     requests 30 · 5xx alert pages 0 · app shows "Time is up" screen
4.1 candidate c-1 opens a-8 (c-2's attempt) and a-99 (no such attempt)
  as shipped         a-8 403 · a-99 404 · the replies tell that a-8 exists: true
  MR !3940           a-8 404 · a-99 404 · the replies tell that a-8 exists: false

The 500 was retried twice per candidate and paged the on-call for a clock that worked as designed (steps 3, 4, 6). The 409 reaches the app once, and the app picks its screen by the problem's type (steps 9, 13). With the shipped replies, a candidate learns that a-8 exists; with the MR's, a-8 and a-99 look the same (step 11).

4.2 The MR's problem body

4.2 the MR's reply to a late submit
  HTTP/1.1 409 · content-type: application/problem+json
  {"type":"https://examhall.example/problems/attempt-closed","title":"Attempt closed","status":409,"detail":"The timer for this attempt ended at 10:30.","instance":"/attempts/a-7/submission"}
  status member 409 = status line 409: true

The type names the problem for the app, title stays the same for every closed attempt, detail is written for the candidate, and instance names this occurrence (step 13). The status member repeats the status line rather than replacing it (step 14).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A streaming service runs two video packagers, pk-1 and pk-2, behind a load balancer. The balancer calls each packager's GET /health every 5 seconds and stops routing to one whose check fails. pk-2 loses its storage mount, so every segment it serves fails. Its health handler catches the error and answers 200 with {"healthy":false,"reason":"storage mount missing"}. Over the next 100 segment requests, half still reach pk-2 and fail for viewers.

Which change makes the balancer stop sending requests to pk-2?

Pick one

Question 2 of 7

A video platform's API receives three subtitle uploads, each PUT /episodes/e-12/subtitles/fr with Content-Type: application/json. In (1), the body stops halfway through a JSON string. In (2), the JSON parses and every field is present, but one cue ends at 00:01:02 and starts at 00:01:05. In (3), the JSON is complete and valid, but e-12 was published an hour ago, and a published episode's subtitle tracks stay locked until an editor unpublishes it.

Which statuses does RFC 9110 describe for (1), (2) and (3)?

Pick one

Question 3 of 7

A streaming app's GET /playlists/{id} answered 403 when a viewer asked for another viewer's private playlist. An MR changes it:

The MR says: "a viewer can no longer tell whether someone else's private playlist exists." Which review comment should block it?

Code for this question
const pl = playlists.get(id);if (!pl) return problem(404, 'not-found', 'Not found', `No playlist ${id}`);if (pl.private && pl.owner !== viewer) return problem(404, 'not-found', 'Not found', `Playlist ${id} is private`);return ok(pl);
Pick one

Question 4 of 7

A creator dashboard loads GET /channels/c-1/stats through ky 2.1.0 with its default retry settings. When a channel has spent its stats quota, the API refuses it until 30 seconds after the first refusal. The team wants the dashboard to get its stats with no code of its own, and wants the on-call paged only for server failures.

Which reply should the API send while the quota is spent?

Pick one

Question 5 of 7

A video app's player calls GET /titles/{id}/stream through ky 2.1.0 with ky's default retry settings, plus the token-refresh hook from ky's readme: in afterResponse, it gets a new access token from POST /session/refresh once and retries the request with it. The streaming API answers an expired access token with 401 and a problem body, and a kids profile's request for an 18-rated film with 403 and a problem body. One evening, 20 viewers whose access tokens have expired (their refresh tokens are still valid) and 10 kids profiles asking for the 18-rated film press Play once each.

How many refresh calls and stream requests reach the API?

Pick one

Question 6 of 7

A box office's ticket ids run in sequence (t-1, t-2, …). GET /tickets/{id} answers one 404 problem, with an instance of /tickets/{id}, both for a missing ticket and for another fan's. POST /tickets/{id}/transfer answers 404 for a missing ticket and 403 "Not your ticket" for another fan's. A review finds that a fan's app can still learn whether a ticket id it holds exists.

Which fix stops that, for any id the app holds?

Pick one

Question 7 of 7

A merchant ledger's GET /balances handler, when the ledger does not answer:

During a ledger outage, 40 merchants open their balances page once each, through ky 2.1.0's defaults. Each page shows the problem's detail after a single request, and the gateway's server-error alert never pages.

Which one-line change lets ky and the alert see the outage?

Code for this question
res.setHeader('content-type', 'application/problem+json');res.end(JSON.stringify({ type: 'https://pay.example/problems/ledger-unavailable',  title: 'Ledger unavailable', status: 503, detail: 'The ledger did not answer.' }));
Pick one

Go deeper

Pro tips
  1. Choose the status from the tools' point of view, then write the body. Ask what a client library, the gateway's alert and the balancer will do with the status before deciding what the problem body says. When: adding an error path to any endpoint (steps 1–4, 13).
  2. Test the status line, not just the body. In a handler test, assert the response status as well as the problem fields, so a handler that writes a correct body with Node's default 200 fails the test. When: writing tests for error paths (step 14).
Source
  • "The first digit of the status code defines the class of response"; 4xx: "The request contains bad syntax or cannot be fulfilled"; 5xx: "The server failed to fulfill an apparently valid request" (steps 1–2). Valid as of 2026-10-12.
  • RFC 9110 §15.5.1 400, §15.5.21 422, §15.5.10 409 (same file). 400: "something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)". 422: "the syntax of the request content is correct, but it was unable to process the contained instructions". 409: "the request could not be completed due to a conflict with the current state of the target resource. This code is used in situations where the user might be able to resolve the conflict and resubmit the request". Valid as of 2026-10-12.
  • RFC 9110 §15.5.2 401, §15.5.4 403, §15.5.5 404 (same file). 401: "the request has not been applied because it lacks valid authentication credentials for the target resource"; 403: "the server understood the request but refuses to fulfill it", and "The client SHOULD NOT automatically repeat the request with the same credentials"; "An origin server that wishes to "hide" the current existence of a forbidden target resource MAY instead respond with a status code of 404 (Not Found)"; 404: the server "did not find a current representation for the target resource or is not willing to disclose that one exists". Valid as of 2026-10-12.
  • "The 429 status code indicates that the user has sent too many requests in a given amount of time"; responses "MAY include a Retry-After header indicating how long to wait before making a new request". Valid as of 2026-10-12.
  • ky 2.1.0 readme: https://raw.githubusercontent.com/sindresorhus/ky/v2.1.0/readme.md · fetched 2026-10-12. Its retry defaults: "limit: 2", "methods: get put head delete options trace query", status codes 408 413 429 500 502 503 504, afterStatusCodes 413 429 503, and "If the status code is not in afterStatusCodes, retry timing headers will be ignored" (step 3). Its hooks.afterResponse example "Or retry with a fresh token on a 401 error" runs if (response.status === 401 && retryCount === 0) before refreshing; P5's hook is that example. Also "413 Payload Too Large is only retried when the response includes a retry timing header" and "Network errors (e.g., DNS failures, connection refused, offline) are automatically retried for retriable methods" (step 3). The readme describes the library; every ky behaviour graded here was run (ky 2.1.0 installed from the npm registry). Valid as of 2026-10-12.
  • Model, not observed: the on-call alert (pages once when more than 5% of a window's responses have a 5xx status line) and the load balancer of P1 (round-robin over instances whose last GET /health returned a 2xx) are models of status-reading software, the "generic HTTP software" RFC 9457 §5 names; no real monitoring product or balancer was run.

Checked against the tools and docs as of 2026-10-12.