Skip to content

fix(backends): give ttl one meaning across backends - #136

Merged
allen0099 merged 1 commit into
masterfrom
fix/ttl-validation
Sep 25, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/ttl-validation

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #102.

Problem

Nothing validated ttl, and each backend read 0 or a negative value differently:

  • Memcached: exptime 0 means never expire.
  • Redis: SET ... EX 0 fails with invalid expire time.
  • Memory: the entry expires at once.

So @cache(ttl=0), a legal max-age=0, answered 500 on every miss with Redis and replayed the first response forever with Memcached.

Change

This implements the direction agreed in the issue comment.

  • validate_ttl(ttl) in fastapi_cachex.backends.base: returns None or a positive ttl unchanged, and raises ValueError for zero or negative values.
    • It is called at the start of set, set_if_absent and increment on Memory, Redis and Memcached, before any I/O.
    • The base-class set_if_absent/increment fallbacks call it too.
    • The abstract set docstring asks third-party backends to use it.
  • CacheManager: validates default_ttl in __init__, and the effective ttl in set/add. get_or_set validates before running the factory.
  • StateManager: validates default_ttl in __init__ and the effective ttl in create_state.
  • @cache:
    • ttl < 0 raises CacheXError at decoration time, like the other argument checks.
    • ttl=0 still sends max-age=0, but the entry is stored like ttl=None: without expiry, and never replayed directly. A matching If-None-Match gets a 304. The backend never receives 0.
  • Docs:
    • docs/BACKENDS.md gets a "TTL values" section.
    • docs/HTTP_CACHING.md and docs/CACHE_FLOW.md describe ttl=0.

SessionManager already clamps its TTL with max(ttl, 1) and is unchanged.

Tests

  • New tests/backends/test_ttl_contract.py:
    • 0 and -1 are rejected by set/set_if_absent/increment on every built-in backend and on the base fallbacks. Redis points at an unconnected port and Memcached's client is a stub, which proves the check happens before I/O.
    • CacheManager (including that the get_or_set factory is not called) and StateManager reject them too.
  • tests/test_cache.py: test_ttl_zero_entry_expires_immediately asserted the old memory-only behaviour. It is replaced by a test that expects max-age=0, a stored entry with no expiry, a handler rerun without a validator, and a 304 with one. A second new test covers the negative-ttl CacheXError.
  • tests/backends/test_redis.py: a live end-to-end @cache(ttl=0) route. Against the old code it fails with the Redis error.
  • tests/backends/test_memcached.py: the (0, 0) case of test_short_ttls_stay_relative recorded the "0 = never expire" behaviour and is removed.

Against the old code, the three @cache tests fail (the contract tests can't import validate_ttl).

Checks

  • ruff check / format on fastapi_cachex, tests and scripts
  • mypy strict on the package; mypy on tests and scripts
  • pytest against live Redis and Memcached (CACHEX_REQUIRE_LIVE_SERVERS=1): 744 passed, 100% coverage
  • zensical build --strict

Compatibility

ttl <= 0 used to "work" on the memory backend, where the entry expired immediately. It now raises. That input already crashed on Redis and meant the opposite on Memcached, so the issue scheduled this for 0.3.x.

Zero or negative TTLs meant 'never expire' on Memcached, raised on Redis
and expired at once in memory. Reject them with ValueError through a
shared validate_ttl() in every backend, the base fallbacks, CacheManager
and StateManager. @cache(ttl=0) keeps sending max-age=0 but stores the
entry like ttl=None, so the backend never sees 0; a negative @cache ttl
raises CacheXError at decoration time.

Closes #102
@allen0099
allen0099 merged commit 036d8ca into master Sep 25, 2026
10 checks passed
@allen0099
allen0099 deleted the fix/ttl-validation 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ttl <= 0 means something different on every backend

1 participant