fix(cache): skip the backend for routes without a positive ttl - #159
Merged
Merged
Conversation
allen0099
force-pushed
the
fix/ttl-none-no-server-cache
branch
2 times, most recently
from
September 26, 2026 11:35
821c0ba to
a88e20d
Compare
With ttl=None or ttl=0 the response was stored without expiry, and a request whose If-None-Match matched the stored ETag got a 304 without the handler running. Once the data changed, a client revalidating with the old ETag kept getting 304 until another request rewrote the entry, and every query-string variant stayed in the backend forever. A ttl that allows no reuse now means no server-side cache: such routes take the private=True path, which neither reads nor writes the backend, runs the handler on every request and answers 304 only when If-None-Match matches the freshly rendered response. Tests that relied on ttl-less no_cache routes storing entries now set ttl=60. Closes #110
allen0099
force-pushed
the
fix/ttl-none-no-server-cache
branch
from
September 26, 2026 11:38
a88e20d to
30c6798
Compare
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 #110.
Problem
With
@cache(ttl=None)(andttl=0), the response was stored without expiry. It was never served directly, but a request whoseIf-None-Matchmatched the stored ETag got a 304 without the handler running. Once the data changed, a client revalidating with the old ETag kept getting 304 until some other request happened to rewrite the entry. The entries also never expired, so every query-string variant accumulated in the backend.Decision
ttlis what is sent asmax-age: how long a stored response may be reused without running the handler. Without a positivettl, no reuse is allowed. A 304 answered from a stored ETag is a form of reuse: it confirms a copy the handler may no longer produce. So a route without a positivettlnow has no server-side cache at all.ETag revalidation itself stays. The handler runs and its fresh ETag is compared with
If-None-Match, which is always correct and still saves the body transfer. This is option A from the issue (compare against a fresh render) combined with option B (do not store).Change
cache.py:bypass_backend = private or not ttl. Routes without a positivettltake the existingprivate=Truepath, which never touches the backend. The now-redundantstore_ttlis gone.clear()removes them.ttlandno_cachedocstrings,docs/HTTP_CACHING.md,docs/CACHE_FLOW.md(including the decision-logic sketch and the scenario table) and the changelog are updated.No API change. The one behaviour change besides the fix: a request with a matching
If-None-Matchnow runs the handler on these routes.Tests
test_without_a_positive_ttl_nothing_is_stored_or_served, run forttl=Noneandttl=0. It replaces the oldttl=0test, which asserted the stored-ETag 304.test_without_a_ttl_an_entry_left_by_an_older_version_is_ignored: a pre-existing entry is neither served nor overwritten.no_cachetests that exercised the stored path through ttl-less routes now usettl=60, so theno_cachebranches stay covered.test_error_does_not_overwrite_a_good_cached_entryalso usesttl=60, no_cache=True, since it needs the handler to run while an entry is stored.With
bypass_backend = privaterestored, the three new tests fail. The full suite passes against live Redis and Memcached (CACHEX_REQUIRE_LIVE_SERVERS=1): 792 passed, 100% coverage.mypy --strictand pre-commit are clean.Like #156, #157 and #158, this adds a
### Fixedsection to## [Unreleased], so later merges need a small CHANGELOG rebase.