Skip to content

fix(cache): reject async key builders instead of failing every request - #341

Merged
allen0099 merged 1 commit into
masterfrom
fix/async-key-builder-323
Sep 28, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/async-key-builder-323

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Summary

@cache(key_builder=...) accepted an async def key builder and then failed every request with a 500 (TypeError: sequence item 0: expected str instance, coroutine found plus "coroutine was never awaited"). invalidate(request, key_builder=...) had the same problem.

Approach: reject async builders rather than await them. The CacheKeyBuilder type is sync, the builder runs in more than one place, and anything async a builder needs can be resolved in a dependency or middleware and read from request.state.

  • Decoration time: @cache raises CacheXError if the builder is a coroutine function, an object with an async def __call__, or a functools.partial (nested partials too) of either. It reuses the existing _is_coroutine_callable check after unwrapping partials.
  • invalidate(): runs the same check and raises before touching the backend.
  • Request time: if a builder the check cannot see through (for example a sync wrapper that returns a coroutine) returns anything but a str, the request raises CacheXError. A returned coroutine is closed first, so no "never awaited" RuntimeWarning appears. invalidate() does the same.
  • fail_open does not apply. It is a programming error in the route, not a backend failure. The builder call already sat outside the fail_open handling, and this is now documented.

Docs: the key_builder / invalidate() docstrings and "Raises" sections, the key builder section of docs/HTTP_CACHING.md and its zh-TW mirror. Changelog fragment changelog.d/323.fixed.md.

Tests

New tests in tests/test_cache.py:

  • decoration-time rejection of an async def, an async callable object, a partial of an async function, and a nested partial of an async callable object
  • invalidate() with an async builder raises without touching the backend
  • a builder returning a coroutine or an int raises CacheXError, both through @cache (with fail_open=True) and through invalidate()

All 9 new test cases fail against master's cache.py and pass with this change. The full suite passes with coverage at 93.9%. ruff, mypy --strict and pre-commit are clean.

Closes #323

@cache(key_builder=...) accepted an async def builder and then failed
every request with a TypeError and a "coroutine was never awaited"
warning. The decorator now raises CacheXError when applied if the builder
is a coroutine function, an object with an async __call__, or a
functools.partial of either. invalidate() raises the same error before
touching the backend.

A builder that still returns a non-str (e.g. a sync wrapper returning a
coroutine) raises CacheXError at request time; a returned coroutine is
closed first. fail_open does not apply: this is a programming error, not
a backend failure.

Closes #323
@allen0099 allen0099 added this to the 0.3.9 milestone Sep 28, 2026
@allen0099 allen0099 added bug Something isn't working http-cache The @cache decorator, cache keys and Cache-Control handling developer-experience Surprising behaviour, missing warnings or unclear errors for library users labels Sep 28, 2026
@allen0099
allen0099 merged commit 0ffdb48 into master Sep 28, 2026
12 checks passed
@allen0099
allen0099 deleted the fix/async-key-builder-323 branch September 28, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working developer-experience Surprising behaviour, missing warnings or unclear errors for library users 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: an async key_builder is accepted, then every request fails with 500

1 participant