Conditional request tester: 200, 304, 412, 206 or 416?

Paste what the client sent — If-None-Match, If-Modified-Since, If-Match, If-Unmodified-Since, If-Range, a method and a Range — and what the server holds. You get the status a correct server returns, every rung of the precedence ladder with its own verdict, and the one rung that actually decided. Two of those rungs are silenced by the rung above them, which is where most of the surprises live.

Runs entirely in your browser. Your data is never uploaded.

The method comes from the request line — without one the request is read as a GET, and the method changes three of the six answers. HTTP/2 pseudo-headers (:method, :path) work, so a paste straight out of your browser's network panel is fine.

The validators of the response that would otherwise be sent: ETag, Last-Modified, and Content-Length when you want a Range answered. A status line here is ignored — working out the status is this page's job.

Examples

Every rung, in the order Go evaluates them

    Whose answer this is

    This question has more than one widely deployed answer, so naming the implementation matters more here than it usually does. Every verdict on this page is Go 1.24.7 net/http's http.ServeContent — the precondition evaluator behind every Go static file server, and a direct implementation of the RFC 9110 §13.2.2 precedence ladder. It was run for real, and 1,474 recorded cases are replayed against this page's engine on every build, so a mistranslation fails a test instead of misleading you.

    It is deliberately not a fresh reading of the specification. A reading is an opinion; a pinned implementation is something we can check ourselves against. Where another implementation answers differently — and fresh 0.5.2, behind Express's req.fresh and Koa, does on one input and lacks three of the five headers entirely — the page says so under the verdict, whenever your own headers reach one. The table further down lists all four.

    The ladder, and the two rungs that silence the rung below

    A server does not weigh the preconditions up. It walks them in a fixed order and the first failure ends the request:

    1. If-Match, or — only if If-Match is absent — If-Unmodified-Since. Either failing is a 412, before anything below is looked at.
    2. If-None-Match, or — only if If-None-Match is absent — If-Modified-Since. A match is a 304 on a GET or HEAD, and a 412 on anything else.
    3. Range, dropped when an If-Range is present and fails. A satisfiable range is a 206; one starting past the end of the body is a 416.

    The words only if in the first two rungs are the whole reason this tool exists. A present If-Match silences If-Unmodified-Since completely — including when the date would have refused the request — and a present If-None-Match silences If-Modified-Since the same way, including when the timestamp says nothing changed. No amount of reading the headers themselves shows you that; the ladder above marks the silenced rung explicitly.

    What each rung reads, and what it does with it

    Every status below is produced by running this page's engine when the page was built, and asserted by a test — so this table cannot drift from the answers above. Paste any row into the form to see its ladder.

    Rung Example Answer What it is for
    If-Match Compared against the response’s ETag, strong comparison Always, when present — and it silences If-Unmodified-Since entirely. PUT /file If-Match: "v1" ETag: "v2" 412 Precondition Failed The lost-update guard. The client is saying "only if the thing is still what I read", and a changed ETag refuses the write.
    If-Unmodified-Since Compared against the response’s Last-Modified, at whole-second precision Only when If-Match is absent. PUT /file If-Unmodified-Since: Fri, 11 Sep 2026 12:00:00 GMT ETag: "v2" Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 412 Precondition Failed The same guard, with a timestamp instead of a validator. A second of resolution is all an HTTP-date carries, so two writes inside one second are invisible to it.
    If-None-Match Compared against the response’s ETag, weak comparison Always, when present — and it silences If-Modified-Since entirely. GET /file If-None-Match: W/"v1" ETag: "v1" 304 Not Modified Weak comparison, so W/"v1" matches a strong "v1" and the client is told to keep what it has. The same pair under If-Match is a 412.
    If-Modified-Since Compared against the response’s Last-Modified, at whole-second precision Only when If-None-Match is absent, and only on a GET or HEAD. GET /file If-Modified-Since: Sat, 12 Sep 2026 12:00:00 GMT Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 304 Not Modified The comparison is <=, so a timestamp exactly equal to Last-Modified counts as not modified and gets a 304.
    If-Range Compared against the response’s ETag (strong) or a date exactly equal to Last-Modified Only on a GET or HEAD, and only to decide whether the Range survives. GET /file If-Range: W/"v1" Range: bytes=0-9 ETag: W/"v1" Content-Length: 100 200 OK Strong comparison, so a weak ETag on both sides fails: the Range is dropped and the whole body is sent. A resumed download starts again from zero, and nothing in the response says why.
    Range Compared against the response’s Content-Length Last, and only if nothing above it produced a 412 or a 304. GET /file Range: bytes=0-9 ETag: "v1" Content-Length: 100 206 Partial Content A satisfiable range is a 206 carrying Content-Range. Without a numeric Content-Length in the response block this tool cannot say which bytes it selects, and answers 200.

    Results that surprise people

    Each answer here is the one this tool gives, computed when the page was built. The first block in every case is the request, the second is what the server holds. Paste any of them above to see which rung decided.

    Case Answer Why it catches people
    A weak validator under If-Match PUT /file If-Match: W/"v1" ETag: W/"v1" 412 Precondition Failed If-Match uses strong comparison, and a weak validator is never strong-equal to anything — not even to a byte-identical copy of itself. The two sides are the same string and the write is still refused.
    An unquoted ETag GET /file If-None-Match: v1 ETag: v1 200 OK An entity-tag must be quoted. v1 is not one, so Go cannot read it on either side and it matches nothing: every conditional request gets a full body, for ever, and no error is reported anywhere.
    A weak validator under If-Range GET /file If-Range: W/"v1" Range: bytes=0-9 ETag: W/"v1" Content-Length: 100 200 OK Strong comparison again, and this time the cost is silent: the Range is discarded and the client is sent all 100 bytes with a 200. A download manager resuming a large file starts from the beginning.
    If-None-Match: * against a response with no ETag GET /file If-None-Match: * 304 Not Modified Go treats the * as a match before it looks at whether there is an ETag at all. A client sending * to mean "only if this does not exist yet" is told the opposite.
    A mismatching If-None-Match beside a matching date GET /file If-None-Match: "old" If-Modified-Since: Sat, 12 Sep 2026 12:00:00 GMT ETag: "new" Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 200 OK The date says nothing changed and the full body is sent anyway: Go reads If-Modified-Since only when If-None-Match is absent. This is the shape behind tornado issue 2262 and the IMS_INM_MISMATCH thread in the HTTP working group.
    A passing If-Match beside a failing If-Unmodified-Since PUT /file If-Match: * If-Unmodified-Since: Fri, 11 Sep 2026 12:00:00 GMT ETag: "v1" Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 200 OK The date would have refused this write on its own. It is never consulted, because If-Match is present — so a client that sends both, believing it has two guards, has one. This is RFC 9110 §13.2.2 step 2.
    A matching If-None-Match on a DELETE DELETE /file If-None-Match: "v1" ETag: "v1" 412 Precondition Failed A 304 is only ever an answer to a GET or a HEAD. On any other method a matching If-None-Match means "the thing you were trying not to overwrite is already there", which is a refusal.
    An ISO-8601 date GET /file If-Modified-Since: 2026-09-12T12:00:00Z Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 200 OK Go parses exactly three date formats, and this is not one of them. A malformed date does not fail the request and is not reported — it removes the 304 you thought you had, and the only symptom is bandwidth.
    A Last-Modified at the Unix epoch GET /file If-Modified-Since: Sat, 12 Sep 2026 12:00:00 GMT Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT 200 OK Go treats both the zero time and the epoch as "no modification time at all", so a server that fell back to 0 when it could not stat a file has, as far as every date precondition is concerned, sent no validator.
    A mismatching If-Range on a PUT PUT /file If-Range: W/"v1" Range: bytes=0-9 ETag: W/"v1" Content-Length: 100 206 Partial Content Content-Range: bytes 0-9/100 The same input on a GET gives 200 with the whole body. checkIfRange runs only for GET and HEAD, so on a PUT the failing If-Range is skipped and the Range it should have cancelled is honoured instead.
    A Range on a POST POST /file Range: bytes=0-9 Content-Length: 100 206 Partial Content Content-Range: bytes 0-9/100 ServeContent does not restrict partial responses to safe methods, so it answers 206 to a POST. Most other servers would not, which makes this a portability trap rather than a rule to rely on.
    A Range past the end of the body GET /file Range: bytes=500-600 ETag: "v1" Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT Content-Length: 100 416 Range Not Satisfiable Content-Range: bytes */100 The 416 carries Content-Range: bytes */100 to say how long the representation really is — and it carries neither the ETag nor the Last-Modified, so a client that only ever sees 416s never learns the validator it needs.
    A 304 that has an ETag and a Last-Modified to choose from GET /file If-None-Match: "v1" ETag: "v1" Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 304 Not Modified The 304 carries the ETag and not the Last-Modified: Go strips it whenever an ETag is present, which RFC 9110 §15.4.5 allows. A trace where the header looks like it went missing is this rule.

    What a 304 and a 416 carry back

    Two rules that look like headers going missing from a trace, and both are properties of the whole recorded corpus rather than of one case — which is why they can be stated flatly.

    Where another implementation answers differently

    These are the four places this engine knows of where a widely deployed implementation does not agree with ServeContent. Each is shown on the page above whenever your own input reaches it, and the "Answer" column is Go's — the other implementation's behaviour is described, not executed, because only Go is pinned by the corpus here.

    Implementation Input that reaches it Go's answer What they do instead
    fresh 0.5.2 (Express req.fresh, Koa) GET /file Cache-Control: no-cache If-None-Match: "v1" ETag: "v1" 304 Not Modified Express and Koa answer 200 here, not 304: fresh treats a request Cache-Control: no-cache as an order to re-send the body, and never reaches the validator comparison. Go ignores request Cache-Control entirely. Both are defensible; they are not the same answer.
    fresh 0.5.2 (Express req.fresh, Koa) PUT /file If-Match: "v1" ETag: "v1" 200 OK fresh implements only If-None-Match and If-Modified-Since. An Express app that has not written the check itself will ignore this header rather than return 412 or 206.
    Apache httpd with mod_deflate GET /file If-None-Match: "v1" ETag: "v1-gzip" 200 OK Apache appends -gzip to the ETag after the comparison, so a compressed response never revalidates and every request gets a full body. DeflateAlterETag NoChange switches it off. This ETag ends in -gzip, which is what that looks like.
    Spring Framework resource handler GET /file If-Modified-Since: Sat, 12 Sep 2026 12:00:00 GMT Last-Modified: Sat, 12 Sep 2026 12:00:00 GMT 304 Not Modified Spring will answer 304 from If-Modified-Since alone after a redeploy has reset file timestamps, serving a stale asset; setUseLastModified(false) exists to switch that off (spring-framework issue 29031).

    Three things this page cannot tell you

    About this tool

    The evaluator is a TypeScript port of Go 1.24.7 net/http, http.ServeContent, pinned by 1,474 recorded cases — 944 ending in 200, 353 in 412, 86 in 304, 58 in 206 and 33 in 416, so no test passes by always giving the same answer. They were generated by running the real thing over an HTTP test server in the build container and replayed against the port on every build. The header parser is ours, shared with the HTTP cache tester, and it reports what it made of your paste rather than guessing quietly.

    Nothing here reads a clock. Every date comes from the headers you paste, and Last-Modified is compared at whole-second precision because that is all an HTTP-date carries — so the same input always gives the same answer, today and next year.

    Everything runs in your browser. Request headers routinely carry session cookies, bearer tokens and internal hostnames, so nothing you paste is uploaded, logged or sent anywhere — which is also why this tool takes pasted headers instead of fetching a URL for you.