Conditional requests & caching¶
Let clients revalidate cheaply: derive an
ETag, honorIf-None-Match/If-Modified-Since, and answer an unchanged resource with a bodyless304.
OGC services are read-heavy and lean on conditional GETs, but FastAPI gives you
nothing for them out of the box. gazebo ships the pieces — and they are strictly
opt-in: nothing changes until you call them in a route. The split mirrors the rest
of the library: the pure logic (hashing a value into an ETag, evaluating the
preconditions) is framework-free in gazebo.caching;
the request/response plumbing is two small helpers in the FastAPI glue.
ETags¶
etag_for(value) reduces a value — a pydantic model, a mapping, a string, or bytes —
to a hash and returns a quoted entity-tag. It is weak (W/"…") by default, because
it hashes a serialization: that signals semantic equivalence, the honest strength for
a content hash. Derive the tag from the underlying data (a row, an updated_at), not
the link-bearing response envelope — a model with deferred links only serializes inside
a request, and you rarely want the ETag to change just because a URL did.
from gazebo.caching import etag_for
# a weak ETag derived from a serialization of the value
tag = etag_for({'id': 1, 'name': 'Fern'})
assert tag.startswith('W/"')
Short-circuiting to 304¶
Inside a route, build the ETag (and/or a Last-Modified), then ask not_modified()
whether the request's preconditions are already satisfied. If they are, it returns a
ready 304 carrying the validators — return it directly. Otherwise stamp the success
response with set_cache_headers() so the next request can be conditional. Inject the
Response parameter so you can set headers while still returning your model:
from gazebo.ext.fastapi import GazeboApp, Providers, etag_for, not_modified, set_cache_headers
app = GazeboApp(Providers())
PLANT = {'id': '1', 'name': 'Fern'}
@app.get('/plants/{plant_id}', response_model=dict)
async def get_plant(plant_id: str, request: Request, response: Response) -> dict | Response:
etag = etag_for(PLANT)
# If the client's cached copy is still current, short-circuit to 304 (no body) —
# passing cache_control so the 304 refreshes the cache's freshness directives too.
if (cached := not_modified(request, etag=etag, cache_control='max-age=300')) is not None:
return cached
# Otherwise stamp the validators so the *next* request can be conditional.
set_cache_headers(response, etag=etag, cache_control='max-age=300')
return PLANT
not_modified() follows RFC 7232: only GET/HEAD are eligible, If-None-Match uses
weak comparison and takes precedence over If-Modified-Since, and HTTP dates are
compared at one-second resolution. When neither precondition matches it returns None,
so the pattern above degrades to an ordinary response.
Reference¶
See gazebo.caching (etag_for, http_date,
is_not_modified) and the glue helpers
not_modified / set_cache_headers.