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:
-
If-Match, or — only ifIf-Matchis absent —If-Unmodified-Since. Either failing is a 412, before anything below is looked at. -
If-None-Match, or — only ifIf-None-Matchis absent —If-Modified-Since. A match is a 304 on aGETorHEAD, and a 412 on anything else. -
Range, dropped when anIf-Rangeis 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.
- A 304 for a resource that has both validators carries only the
ETag. Of the 86 recorded 304s, 72 came from a resource holding anETagand aLast-Modified, and not one of them carried the Last-Modified back. RFC 9110 §15.4.5 permits this. A 304 for a resource with noETagdoes carry the date. - A 416 carries neither validator. Of the 33 recorded
416s, 24 were for a resource that had an
ETag, and none of the 416s carried an ETag or a Last-Modified at all. A client whose ranges are always out of bounds therefore never learns the validator that would let it ask correctly — it only getsContent-Range: bytes */<length>, which is the length it needed.
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
- What your server actually does. This is Go's evaluator. Nginx, Apache, Express, Spring, a CDN and your own handler are six more implementations, and the table above names four places they part company. When the answer matters, send the request to the server you have.
- Whether the response would have been cached in the first place. A conditional request only happens because something stored a copy and its freshness ran out. That is a separate question, and the HTTP cache tester answers it.
- What the body looks like. The
Content-Lengthyou paste is used only to work out which bytes aRangeselects and whether it is satisfiable. Nothing here reads or produces a body, and a multipart 206 is reported as multipart rather than assembled.
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.