Skip to content

Conditional requests

Which resources carry an ETag, what it is derived from, and how to send one back.

The read resources carry an ETag and answer a conditional request. Read the validator off the response, send it back as If-None-Match, and a representation you already hold costs you a round trip and no body.

Resource Carries ETag
GET /api/pages Yes
GET /api/pages/{path} Yes
GET /api/query-schema.json Yes
GET /api/openapi.json Yes
GET /.well-known/api-catalog Yes
GET /api/collections Yes
GET /api/site-pages Yes
GET /api/tags Yes
GET /api/problems/{slug} Yes
QUERY /api/pages No — see below
GET /api/feedback No

The generated reference documents the 304 on every one of them, and a test compares this table against that document — so a resource gaining or losing a validator fails naming itself rather than leaving this table quietly wrong. Writing this page is what found the two the document was missing: /api/pages/{path} and /api/problems/{slug} both set a validator and always answered a conditional request, and the document had never said so.

How to use one

curl -sS -D- https://lateano.com/api/collections
# ETag: "…"

curl -sS -D- -H 'If-None-Match: "…"' https://lateano.com/api/collections
# HTTP/2 304

A 304 carries ETag and Cache-Control and no body. It means the representation you already have is still current.

What the validator is derived from

The commit these bytes were built from, and the corpus that was baked into them — the same two facts /version.json publishes, described under Versioning, so a client comparing a validator against that marker is comparing the same things rather than two derivations of them.

That makes these strong validators. The corpus is fetched during the build and emitted into the bundle, and nothing reads a content store at request time, so two deployments built from one commit and one corpus answer byte-identically. The validator can therefore promise byte equality, which is what a strong validator claims.

A build that cannot stamp itself carries no ETag at all. Both stamps have to be real. If either is missing the resource omits the header rather than emitting a constant tag — a tag that cannot tell two representations apart would answer 304 for something that has since changed, which is the one failure worth spending a round trip to avoid. A local or fixtures build is the usual case.

Why the query surface cannot have one

QUERY /api/pages carries the question in a request body, so two callers sending different bodies to that one URL receive different answers. An ETag identifies a representation of a resource, and a resource with more than one has nothing for a single validator to name.

The GET spelling beside it does have one, and that is the reason to prefer it: the whole question lives in the URL, so the address has exactly one representation. Reach for GET /api/pages where it can spell your question, and fall back to the QUERY grammar only for what it cannot — a boolean tree, a batch, a lookup by slug, or a pasted passage.

About the 412

The catalogue carries a precondition-failed type, and RFC 9110 § 13.1.2 is why: a failed If-None-Match on a method that is not GET or HEAD takes 412 rather than 304.

You cannot currently provoke it. The non-retrieval surfaces that reach the precondition — QUERY /api/pages and the feedback writes — deliberately carry no validator, so there is nothing for a failed condition to be false against. The other non-retrieval method every resource answers is OPTIONS, and it is answered before any conditional header is read: RFC 9110 § 13.2.1 says a server “MUST ignore the conditional request header fields … when received with a request method that does not involve the selection or modification of a selected representation”, and OPTIONS is named there.

The arm exists so that the day a write gains a validator, the status is already right, rather than a 304 being sent where a client would misread it.

---
title: "Conditional requests"
description: "Which resources carry an ETag, what it is derived from, and how to send one back."
url: "https://lateano.com/developers/api/conditional-requests"
author: "Colin Lateano"
---

# Conditional requests

Which resources carry an ETag, what it is derived from, and how to send one back.

**The read resources carry an `ETag` and answer a conditional request.** Read the
validator off the response, send it back as `If-None-Match`, and a representation
you already hold costs you a round trip and no body.

| Resource                       | Carries `ETag` |
| :----------------------------- | :------------- |
| `GET /api/pages`               | Yes            |
| `GET /api/pages/{path}`        | Yes            |
| `GET /api/query-schema.json`   | Yes            |
| `GET /api/openapi.json`        | Yes            |
| `GET /.well-known/api-catalog` | Yes            |
| `GET /api/collections`         | Yes            |
| `GET /api/site-pages`          | Yes            |
| `GET /api/tags`                | Yes            |
| `GET /api/problems/{slug}`     | Yes            |
| `QUERY /api/pages`             | No — see below |
| `GET /api/feedback`            | No             |

The generated reference documents the `304` on every one of them, and a test
compares this table against that document — so a resource gaining or losing a
validator fails naming itself rather than leaving this table quietly wrong.
Writing this page is what found the two the document was missing:
`/api/pages/{path}` and `/api/problems/{slug}` both set a validator and always
answered a conditional request, and the document had never said so.

## How to use one

```sh
curl -sS -D- https://lateano.com/api/collections
# ETag: "…"

curl -sS -D- -H 'If-None-Match: "…"' https://lateano.com/api/collections
# HTTP/2 304
```

A `304` carries `ETag` and `Cache-Control` and no body. It means the
representation you already have is still current.

## What the validator is derived from

The commit these bytes were built from, and the corpus that was baked into them —
the same two facts `/version.json` publishes, described under
[Versioning](/developers/api/versioning), so a client comparing a validator
against that marker is comparing the same things rather than two derivations of
them.

That makes these **strong** validators. The corpus is fetched during the build
and emitted into the bundle, and nothing reads a content store at request time,
so two deployments built from one commit and one corpus answer byte-identically.
The validator can therefore promise byte equality, which is what a strong
validator claims.

**A build that cannot stamp itself carries no `ETag` at all.** Both stamps have to
be real. If either is missing the resource omits the header rather than emitting
a constant tag — a tag that cannot tell two representations apart would answer
`304` for something that has since changed, which is the one failure worth
spending a round trip to avoid. A local or fixtures build is the usual case.

## Why the query surface cannot have one

`QUERY /api/pages` carries the question in a request body, so two callers sending
different bodies to that one URL receive different answers. An `ETag` identifies
_a representation of a resource_, and a resource with more than one has nothing
for a single validator to name.

**The `GET` spelling beside it does have one**, and that is the reason to prefer
it: the whole question lives in the URL, so the address has exactly one
representation. Reach for `GET /api/pages` where it can spell your question, and
fall back to the `QUERY` grammar only for what it cannot — a boolean tree, a
batch, a lookup by slug, or a pasted passage.

## About the 412

The catalogue carries a `precondition-failed` type, and RFC 9110 § 13.1.2 is why:
a failed `If-None-Match` on a method that is not `GET` or `HEAD` takes `412`
rather than `304`.

**You cannot currently provoke it.** The non-retrieval surfaces that reach the
precondition — `QUERY /api/pages` and the `feedback` writes — deliberately carry
no validator, so there is nothing for a failed condition to be false against. The
other non-retrieval method every resource answers is `OPTIONS`, and it is
answered before any conditional header is read: RFC 9110 § 13.2.1 says a server
"MUST ignore the conditional request header fields … when received with a request
method that does not involve the selection or modification of a selected
representation", and `OPTIONS` is named there.

The arm exists so that the day a write gains a validator, the status is already
right, rather than a `304` being sent where a client would misread it.