Repository navigation
feat(cache): answer HEAD requests from the cached GET response - #437
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #253.
What changes
On a route that accepts HEAD (
@app.api_route(..., methods=["GET", "HEAD"]);@app.getregisters GET only),@cachenow treats HEAD like GET:ETag,Ageand the stored body'sContent-Length, without running the handler. A matchingIf-None-Matchgets a 304.GET(_as_get), so custom key builders built onbuild_cache_keyshare the entry too.stateand the session live in the scope, so the builder sees them.Cache-Control,ETag,Varyand 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.private,ttl=0,no_store,no_cacheandvaryapply 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-Lengthright; 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-ControlorVaryfrom 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 andVarymethod checks in turn, and every one made at least one test fail.Docs
New "HEAD requests" section in
docs/HTTP_CACHING.md, updates todocs/CACHE_FLOW.md, with the zh-TW copies;changelog.d/253.added.md.