Skip to content

HTTP Status Codes

A status code is the server's own statement about what it did with a request. It is evidence about the server's handling, not a verdict about the request's content: it says which layer answered, whether the target was recognised, and whether an identity had been established by the time the decision was made. Most of the value is in the distinctions between neighbouring codes, because those are what change your model of the server.

How the panels render a code

WhereForm it takes
Network log, Status columnThe number alone, coloured by class
Network log, Response sub-tabA status line reading HTTP/1.1 <code> — no reason phrase
Repeater response paneA status line reading HTTP/1.1 <code> — the reason phrase slot is always empty

Neither form shows a reason phrase. The Repeater status line keeps a slot for one, but nothing in this lab ever fills it, so what you read is the code followed by a trailing space; the Network detail view leaves the slot out altogether. On the wire a reason phrase is chosen by the server and is advisory — it is not part of the status the client acts on — but no response here carries one.

Colour bands in the Network log:

ClassColour
500 and aboveRed
400–499Orange
300–399Yellow
Everything below 300, including 2xx and 1xxGreen

The colour is derived from the number by range comparison, so a code nobody has ever seen still lands in a band, and 1xx shares the green band with 2xx instead of having one of its own. Where these controls sit in the panel is covered in the Network panel guide.

2xx — the request reached a handler and the handler answered

A 2xx says the request was well-formed enough to be routed, that whatever authorisation the server wanted was satisfied, and that the server considers its own processing complete.

What a 2xx does not establish: that the application agreed with the request's content. A login form that rejects the password, a search that found nothing, and an API that recorded a validation error can all answer 200 with the disagreement written into the body. Status class and application-level outcome are separate channels, and many applications only use the second one.

CodeWhat the server statesWhat it rules out
200 OKA handler ran and produced a bodyThe path being unrouted; an authorisation gate in front of the handler
201 CreatedA handler ran and reports that a new resource now existsThe request having been a read-only operation
202 AcceptedThe request was taken but processing is not finishedThat the body reflects the final result
204 No ContentA handler ran and deliberately produced no bodyThat an empty response pane means a failed request
206 Partial ContentThe server honoured a range requestThat the body is the whole resource

204 and an empty 200 look identical in a response pane that shows only the body. The status line is the only thing separating "the server chose to send nothing" from "the server sent nothing it meant to send".

3xx — the server is redirecting the client rather than answering

A 3xx says the server recognised the request and is naming a different location, or asserting the client's cached copy is still valid. The path was routed. Whether authorisation was checked before the redirect was chosen is not stated by the code.

CodeWhat the server statesWhat it rules out
301 Moved PermanentlyThe resource has a new canonical location; clients may cache the moveThe original path being the live one
302 FoundA one-off redirect to another location; the original path stays canonicalNothing about the durability of the move
303 See OtherFetch the other location with GET, whatever this request's method wasThe other location being a continuation of the same operation
304 Not ModifiedThe conditional request's validator matched; no body followsThat an empty body means an empty resource
307 Temporary RedirectSame as 302, but the method and body must be preservedThe redirect silently degrading a POST into a GET
308 Permanent RedirectSame as 301, but the method and body must be preservedThe same degradation, for the permanent case

301 vs 302 vs 307

Two independent axes are packed into these three codes, and confusing them makes redirect chains look non-deterministic.

Move is permanentMove is temporary
Method may be rewritten to GET301302
Method and body must be preserved308307

301 and 302 were specified before method preservation was pinned down, and clients settled on rewriting the follow-up request to GET. 307 and 308 exist to state the opposite explicitly. So a POST answered with 302 and a POST answered with 307 produce different second requests — the first arrives as a GET with no body, the second as a POST carrying the original body.

Cacheability is the other consequence: a client that took 301 seriously may skip the original URL entirely on later visits, so a request you believe you sent may never appear.

What a 302 after a form submission establishes

A redirect issued in response to a credential submission establishes that the handler ran and chose to move the client. It does not establish that authentication succeeded — an application is equally free to redirect back to the login page on failure. The distinguishing evidence is the target in the Location header and any Set-Cookie on the redirect response, not the status code.

How the Browser panel treats redirects

The panel follows up to five consecutive 3xx responses. Each hop is dispatched as its own request, so a chain of hops leaves one row per hop in the Network log while the whole chain collapses to a single history entry at the final URL.

Every followed hop is re-issued as GET, regardless of the code that triggered it and regardless of the original method. The panel's behaviour therefore cannot distinguish 302 from 307; the codes themselves, in the Network log's Status column, are the only place that difference is visible.

Two cases end the chain without an error message:

  • A 3xx with no Location header is not followed. It is rendered as an ordinary response, which for a bodiless redirect is a blank page.
  • The sixth consecutive 3xx is rendered the same way — no error is shown, and the URL bar stays at the URL that produced that hop rather than advancing.

4xx — the server rejected the request and blames the request

A 4xx says the server processed the request far enough to decide against it. That decision itself is information: it means something at that path was listening, and it means the rejection was deliberate rather than a crash.

CodeWhat the server statesWhat it rules out
400 Bad RequestThe request could not be parsed or violates a syntactic requirementThe request having reached application logic in a usable form
401 UnauthorizedCredentials are required and were absent or not acceptedThat the resource is open; that identity has been established
403 ForbiddenThe request was understood and refusedThat an accepted identity is sufficient for this resource
404 Not FoundThe server has no representation for this targetNothing about whether the target exists behind an authorisation layer
405 Method Not AllowedThe path is routed but not for this methodThe path being absent — it exists for some other method
409 ConflictThe request collides with the current state of the resourceThe request being malformed
413 Content Too LargeThe body exceeded a configured limitThe body having been parsed
415 Unsupported Media TypeThe Content-Type is not one this handler acceptsThe body's content having been examined
422 Unprocessable ContentThe syntax parsed but the semantics were rejectedA parsing failure — this is a validation decision
429 Too Many RequestsA rate limit was hitThe individual request being invalid

401 vs 403

The difference is whether an identity had been established when the decision was made.

401403
Identity at decision timeAbsent or not acceptedEstablished, or deemed irrelevant
What the server is saying"I do not know who you are""I know enough, and the answer is still no"
WWW-Authenticate headerRequired by the specificationNot expected

A 401 establishes that the path is behind an authentication gate, which means the path exists and the server knows how to route it. A 403 establishes that identity was not the missing piece — the same request with better credentials would be answered by the same rule.

The two are frequently mixed up by applications, so an application-issued 403 where a 401 is expected is itself an observation about that application's own consistency rather than a fact about its access model.

404 vs 403

Both say "you are not getting this", and the difference is what the server is willing to reveal.

  • A 403 confirms the target exists as far as the routing layer is concerned, and that a rule refused it.
  • A 404 states nothing about existence. Servers issue 404 for paths that were never routed, and also for paths that exist but which the configuration hides from unauthorised clients — the second case is 403 semantics wearing a 404.

The consequence is asymmetric: a 403 is positive evidence of existence, while a 404 is evidence of nothing except the absence of a served representation. Comparing the response's size, headers and timing against a path known to be unrouted is what separates the two cases; the code alone does not.

405 as an existence signal

405 is the least ambiguous of the four. It can only be produced by a router that matched the path and then found no handler for the method, so it establishes both that the path is routed and that at least one other method is handled there. The Allow header, when present, names those methods outright.

5xx — the server failed, and says so

A 5xx says the request was accepted as the client's responsibility being met and something on the server side then failed. The class boundary matters: a 4xx is a decision, a 5xx is a malfunction. That distinction is what makes a 5xx informative — it usually means an input reached code that was not written to handle it.

CodeWhat the server statesWhat it rules out
500 Internal Server ErrorThe application itself raised an unhandled conditionThe request having been rejected by a validation rule
501 Not ImplementedThe server does not support the method at allThe failure being specific to this path
502 Bad GatewayA proxy received an invalid or absent answer from upstreamThe failure being in the layer that answered you
503 Service UnavailableThe answering layer is up but declares itself unable to serveThe request having been processed
504 Gateway TimeoutA proxy waited for upstream and gave upThe upstream having refused — it did not answer at all

500 vs 502 vs 504

All three mean "no useful answer", and they name three different failing parties.

Who failedWhat was reached
500The application handling the requestThe application ran and raised something
502An upstream hop, answering badlyThe proxy got a reply it could not use
504An upstream hop, not answeringThe proxy got no reply within its limit

A 500 is the only one of the three that establishes the application executed your input. A 502 and a 504 establish that a proxy stands in front of something, and that the something behind it is in a different state from the layer you are talking to. Between them, 502 means an answer arrived and was rejected, while 504 means the wait ran out — the second is bounded by a timeout, so the elapsed time in the Network log's Time column is itself part of the evidence.

What a 500 body carries

An unhandled server-side exception often serialises the exception into the response body: a stack trace, a file path, a template name, or a database driver's error text. That body states what the application was doing when it broke, and a database error message in particular states that the input reached a query — the criterion for recognising that is covered in SQL injection.

A 500 with an empty or generic body establishes the malfunction without naming it. The absence of detail is a property of the error handler, not of the failure.

Codes this lab produces itself

Some responses in the Network log never came from a challenge application. They are generated at the lab's own dispatch boundary, and reading them as the application's answer will send you looking for behaviour that does not exist.

CodeBodyWhat it means
503{"error": "runtime not ready"}The page's dispatch layer had no runtime yet. The request never reached the challenge application
503{"error": "<the host's own error text>"}The runtime host had no worker to hand the request to. Unlike the other three rows the error value is not a fixed string — it is whatever message the host threw. A response is synthesised deliberately so the attempt still appears as a row rather than vanishing from the log
508{"error": "challenge runtime execution exceeded the time limit"}The challenge application was handed the request and did not answer within the ten-second per-request ceiling
502{"error": "the challenge runtime could not be reached"}Seen by requests in the Code panel: the runtime was unreachable. It arrives as a response object, not as a raised exception

A separate signal sits above these: the PHP runtime, and only it, can add a red block headed Runtime diagnostic: to the expanded Network detail. It reports what that runtime threw away while handling the response — output attached to a status code that cannot carry a body, a reserved header it stripped, and the like — so it is not a sign that the interpreter errored. Every challenge in this repository runs on a Python backend, so none of them can produce it today.

Two consequences follow. First, a red row in the log is not automatically evidence about the challenge application. Second, a 502 seen inside the Code panel and a 502 seen from a challenge application mean different things, and the body text is what separates them.