Skip to content

feat(cache): answer HEAD requests from the cached GET response - #437

Merged
allen0099 merged 1 commit into
masterfrom
feat/253-head-from-get
Oct 5, 2026
Merged

allen0099 merged 1 commit into
masterfrom
feat/253-head-from-get

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #253.

What changes

On a route that accepts HEAD (@app.api_route(..., methods=["GET", "HEAD"]); @app.get registers GET only), @cache now treats HEAD like GET:

  • Hit: a HEAD request reads the entry a GET stored under the same key (RFC 9110 §9.3.2) and gets the stored status and headers, ETag, Age and the stored body's Content-Length, without running the handler. A matching If-None-Match gets a 304.
  • Key: the key builder is called with a copy of the request whose method is GET (_as_get), so custom key builders built on build_cache_key share the entry too. state and the session live in the scope, so the builder sees them.
  • Miss: the handler runs and the response gets the same Cache-Control, ETag, Vary and dependency-header handling as a GET, but it is never stored. A handler may skip the body for HEAD, and a later GET hit would then replay the empty body. An existing GET entry is left alone.
  • Credentials, private, ttl=0, no_store, no_cache and vary apply as for GET.

The full stored response is returned and the server drops the body for HEAD, the way Starlette handles HEAD for any route. This keeps Content-Length right; emptying the body in the decorator would make it 0, and a body-rewriting middleware could change it too.

Behaviour change

A route that already accepted HEAD ran the handler on every HEAD request and got no ETag, Cache-Control or Vary from the decorator. It now skips the handler on a hit. This is the point of the issue, so it is filed under Added; the changelog and docs call it out.

Tests

tests/test_cache_head.py (13 tests): hit, 304 from the entry, a miss that stores nothing, a custom key builder, vary, credentials, a dependency cookie, a different GET entry left alone, no_cache, a 304 from a fresh HEAD render, no_store, private, and POST still bypassing. I broke each of the gate, _as_get, the store skip, the dependency-header and Vary method checks in turn, and every one made at least one test fail.

Docs

New "HEAD requests" section in docs/HTTP_CACHING.md, updates to docs/CACHE_FLOW.md, with the zh-TW copies; changelog.d/253.added.md.

A route that accepts HEAD now goes through @cache like GET. A HEAD request
reads the entry a GET stored under the same key (RFC 9110 §9.3.2): the key
builder is called with the request's method set to GET, so custom builders
share the entry too. A hit returns the stored response without running the
handler, and the server drops the body; a matching If-None-Match gets a 304.

On a miss the handler runs and the response gets the same Cache-Control,
ETag, Vary and dependency-header handling as a GET, but it is never stored:
a handler may skip the body for HEAD, and a later GET hit would replay it.
@allen0099 allen0099 added enhancement New feature or request http-cache The @cache decorator, cache keys and Cache-Control handling labels Oct 3, 2026
@allen0099 allen0099 added this to the 0.4.2 milestone Oct 3, 2026
@allen0099
allen0099 merged commit fb421fd into master Oct 5, 2026
15 checks passed
@allen0099
allen0099 deleted the feat/253-head-from-get branch October 5, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request http-cache The @cache decorator, cache keys and Cache-Control handling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@cache: answer HEAD requests from the cached GET response

1 participant