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
| Where | Form it takes |
|---|---|
Network log, Status column | The number alone, coloured by class |
Network log, Response sub-tab | A status line reading HTTP/1.1 <code> — no reason phrase |
| Repeater response pane | A 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:
| Class | Colour |
|---|---|
| 500 and above | Red |
| 400–499 | Orange |
| 300–399 | Yellow |
| Everything below 300, including 2xx and 1xx | Green |
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.
| Code | What the server states | What it rules out |
|---|---|---|
200 OK | A handler ran and produced a body | The path being unrouted; an authorisation gate in front of the handler |
201 Created | A handler ran and reports that a new resource now exists | The request having been a read-only operation |
202 Accepted | The request was taken but processing is not finished | That the body reflects the final result |
204 No Content | A handler ran and deliberately produced no body | That an empty response pane means a failed request |
206 Partial Content | The server honoured a range request | That 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.
| Code | What the server states | What it rules out |
|---|---|---|
301 Moved Permanently | The resource has a new canonical location; clients may cache the move | The original path being the live one |
302 Found | A one-off redirect to another location; the original path stays canonical | Nothing about the durability of the move |
303 See Other | Fetch the other location with GET, whatever this request's method was | The other location being a continuation of the same operation |
304 Not Modified | The conditional request's validator matched; no body follows | That an empty body means an empty resource |
307 Temporary Redirect | Same as 302, but the method and body must be preserved | The redirect silently degrading a POST into a GET |
308 Permanent Redirect | Same as 301, but the method and body must be preserved | The 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 permanent | Move is temporary | |
|---|---|---|
Method may be rewritten to GET | 301 | 302 |
| Method and body must be preserved | 308 | 307 |
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
Locationheader 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.
| Code | What the server states | What it rules out |
|---|---|---|
400 Bad Request | The request could not be parsed or violates a syntactic requirement | The request having reached application logic in a usable form |
401 Unauthorized | Credentials are required and were absent or not accepted | That the resource is open; that identity has been established |
403 Forbidden | The request was understood and refused | That an accepted identity is sufficient for this resource |
404 Not Found | The server has no representation for this target | Nothing about whether the target exists behind an authorisation layer |
405 Method Not Allowed | The path is routed but not for this method | The path being absent — it exists for some other method |
409 Conflict | The request collides with the current state of the resource | The request being malformed |
413 Content Too Large | The body exceeded a configured limit | The body having been parsed |
415 Unsupported Media Type | The Content-Type is not one this handler accepts | The body's content having been examined |
422 Unprocessable Content | The syntax parsed but the semantics were rejected | A parsing failure — this is a validation decision |
429 Too Many Requests | A rate limit was hit | The individual request being invalid |
401 vs 403
The difference is whether an identity had been established when the decision was made.
401 | 403 | |
|---|---|---|
| Identity at decision time | Absent or not accepted | Established, 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 header | Required by the specification | Not 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
403confirms the target exists as far as the routing layer is concerned, and that a rule refused it. - A
404states nothing about existence. Servers issue404for paths that were never routed, and also for paths that exist but which the configuration hides from unauthorised clients — the second case is403semantics wearing a404.
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.
| Code | What the server states | What it rules out |
|---|---|---|
500 Internal Server Error | The application itself raised an unhandled condition | The request having been rejected by a validation rule |
501 Not Implemented | The server does not support the method at all | The failure being specific to this path |
502 Bad Gateway | A proxy received an invalid or absent answer from upstream | The failure being in the layer that answered you |
503 Service Unavailable | The answering layer is up but declares itself unable to serve | The request having been processed |
504 Gateway Timeout | A proxy waited for upstream and gave up | The 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 failed | What was reached | |
|---|---|---|
500 | The application handling the request | The application ran and raised something |
502 | An upstream hop, answering badly | The proxy got a reply it could not use |
504 | An upstream hop, not answering | The 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.
| Code | Body | What 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.