Skip to content

fix(cache): skip the backend for routes without a positive ttl - #159

Merged
allen0099 merged 1 commit into
masterfrom
fix/ttl-none-no-server-cache
Sep 26, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/ttl-none-no-server-cache

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #110.

Problem

With @cache(ttl=None) (and ttl=0), the response was stored without expiry. It was never served directly, but 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 some other request happened to rewrite the entry. The entries also never expired, so every query-string variant accumulated in the backend.

Decision

ttl is what is sent as max-age: how long a stored response may be reused without running the handler. Without a positive ttl, 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 positive ttl now 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 positive ttl take the existing private=True path, which never touches the backend. The now-redundant store_ttl is gone.
  • Entries that 0.3.7 stored for such routes are neither read nor refreshed. The changelog mentions that clear() removes them.
  • The ttl and no_cache docstrings, 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-Match now runs the handler on these routes.

Tests

  • test_without_a_positive_ttl_nothing_is_stored_or_served, run for ttl=None and ttl=0. It replaces the old ttl=0 test, which asserted the stored-ETag 304.
    • Nothing is stored.
    • An unchanged response still gets 304.
    • After the data changes, the old ETag gets 200 with the new body.
  • test_without_a_ttl_an_entry_left_by_an_older_version_is_ignored: a pre-existing entry is neither served nor overwritten.
  • no_cache tests that exercised the stored path through ttl-less routes now use ttl=60, so the no_cache branches stay covered. test_error_does_not_overwrite_a_good_cached_entry also uses ttl=60, no_cache=True, since it needs the handler to run while an entry is stored.

With bypass_backend = private restored, the three new tests fail. The full suite passes against live Redis and Memcached (CACHEX_REQUIRE_LIVE_SERVERS=1): 792 passed, 100% coverage. mypy --strict and pre-commit are clean.

Like #156, #157 and #158, this adds a ### Fixed section to ## [Unreleased], so later merges need a small CHANGELOG rebase.

@allen0099 allen0099 added this to the 0.3.8 milestone Sep 26, 2026
@allen0099 allen0099 added bug Something isn't working http-cache The @cache decorator, cache keys and Cache-Control handling labels Sep 26, 2026
@allen0099
allen0099 force-pushed the fix/ttl-none-no-server-cache branch 2 times, most recently from 821c0ba to a88e20d Compare September 26, 2026 11:35
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
allen0099 force-pushed the fix/ttl-none-no-server-cache branch from a88e20d to 30c6798 Compare September 26, 2026 11:38
@allen0099
allen0099 merged commit 713f3e1 into master Sep 26, 2026
11 checks passed
@allen0099
allen0099 deleted the fix/ttl-none-no-server-cache branch September 26, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working http-cache The @cache decorator, cache keys and Cache-Control handling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entries cached with ttl=None can answer 304 indefinitely

1 participant