Community-maintained stable fork of fastapi-cache2. This is an alpha (
0.3.0a1) based onv0.2.2. It is experimental and not recommended for production. Thefastapi_cacheimport and public API are unchanged. See COMPATIBILITY.md, MIGRATION.md, SUPPORT.md, SECURITY.md and FORK_ORIGIN.md. Not affiliated with the original author.
Implemented in this alpha: error responses are not cached, opt-in
TypedJsonCoderfor Pydantic v2, and theclear()fixes. The auth/privacy bypass and the InMemoryexpire=None/instance-isolation fixes are implemented onstableand are targeted for0.3.0a2; ETag, non-JSON bypass and_spec_typefixes remain targeted for later0.3.0pre-releases; see MIGRATION.md.
Branch model:
stableis the only maintained and release branch.mainmirrors the upstream project and is not a second development branch.
fastapi-cache is a tool to cache FastAPI endpoint and function results, with
backends supporting Redis, Memcached, and Amazon DynamoDB.
- Supports
redis,memcache,dynamodb, andin-memorybackends. - Easy integration with FastAPI.
- Inherited
ETagandCache-Controlsupport, including conditionalIf-None-Matchrequests.Cache-Controlnow emits only validmax-agevalues; stableETagis targeted for a later0.3.0pre-release.
Alpha caveat:
ETagcorrectness is not fixed in0.3.0a1. The auth/privacy bypass, the InMemoryexpire=None/instance-isolation fixes, and theCache-Controlmax-agefix are implemented onstable(unreleased; target0.3.0a2). See COMPATIBILITY.md.
- FastAPI
rediswhen usingRedisBackend.memcachewhen usingMemcacheBackend.aiobotocorewhen usingDynamoBackend.
0.3.0a1 is a pre-release. Install the exact version:
> pip install "fastapi-cache-stable==0.3.0a1"or
> pip install "fastapi-cache-stable[redis]==0.3.0a1"or
> pip install "fastapi-cache-stable[memcache]==0.3.0a1"or
> pip install "fastapi-cache-stable[dynamodb]==0.3.0a1"from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import Response
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache
from redis import asyncio as aioredis
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
redis = aioredis.from_url("redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
yield
app = FastAPI(lifespan=lifespan)
@cache()
async def get_cache():
return 1
@app.get("/")
@cache(expire=60)
async def index():
return dict(hello="world")First you must call FastAPICache.init during startup FastAPI startup; this is where you set global configuration.
If you want cache a FastAPI response transparently, you can use the @cache
decorator between the router decorator and the view function.
| Parameter | type | default | description |
|---|---|---|---|
expire |
int |
sets the caching time in seconds | |
namespace |
str |
"" |
namespace to use to store certain cache items |
coder |
Coder |
JsonCoder |
which coder to use, e.g. JsonCoder |
key_builder |
KeyBuilder callable |
default_key_builder |
which key builder to use |
injected_dependency_namespace |
str |
__fastapi_cache |
prefix for injected dependency keywords. |
cache_status_header |
str |
X-FastAPI-Cache |
Name for the header on the response indicating if the request was served from cache; either HIT or MISS. |
cache_authenticated |
bool |
None |
per-endpoint override for caching authenticated requests; global default is False; requires an identity-aware key_builder. |
You can also use the @cache decorator on regular functions to cache their result.
The cache decorator injects dependencies for the Request and Response
objects, so that it can add cache control headers to the outgoing response, and
return a 304 Not Modified response when the incoming request has a matching
If-Non-Match header. This only happens if the decorated endpoint doesn't already
list these dependencies already.
The keyword arguments for these extra dependencies are named
__fastapi_cache_request and __fastapi_cache_response to minimize collisions.
Use the injected_dependency_namespace argument to @cache to change the
prefix used if those names would clash anyway.
The default JsonCoder stores any data type that FastAPI can convert to JSON, including
Pydantic models and dataclasses. On a cache hit it returns the decoded JSON value (plain
dicts/lists), not the annotated model; this is the legacy fastapi-cache2 behavior.
Pydantic v2 users can opt into typed restoration with TypedJsonCoder:
from fastapi_cache import FastAPICache
from fastapi_cache.coder import TypedJsonCoder
FastAPICache.init(backend, coder=TypedJsonCoder)or per endpoint:
from .models import SomeModel, create_some_model
@app.get("/foo")
@cache(expire=60, coder=TypedJsonCoder)
async def foo() -> SomeModel:
return create_some_model()TypedJsonCoder uses the endpoint return annotation to restore the annotated type and
propagates validation errors. It requires Pydantic v2; Pydantic v1 and annotations that
cannot be represented by a Pydantic v2 schema keep the legacy decoded behavior.
For broader type support, use fastapi_cache.coder.PickleCoder or implement a custom coder
(see below).
By default use JsonCoder, you can write custom coder to encode and decode cache result, just need
inherit fastapi_cache.coder.Coder.
from typing import Any
import orjson
from fastapi.encoders import jsonable_encoder
from fastapi_cache import Coder
class ORJsonCoder(Coder):
@classmethod
def encode(cls, value: Any) -> bytes:
return orjson.dumps(
value,
default=jsonable_encoder,
option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY,
)
@classmethod
def decode(cls, value: bytes) -> Any:
return orjson.loads(value)
@app.get("/")
@cache(expire=60, coder=ORJsonCoder)
async def index():
return dict(hello="world")By default the default_key_builder builtin key builder is used; this creates a
cache key from the function module and name, and the positional and keyword
arguments converted to their repr() representations, encoded as a MD5 hash.
You can provide your own by passing a key builder in to @cache(), or to
FastAPICache.init() to apply globally.
For example, if you wanted to use the request method, URL and query string as a cache key instead of the function identifier you could use:
def request_key_builder(
func,
namespace: str = "",
*,
request: Request = None,
response: Response = None,
*args,
**kwargs,
):
return ":".join([
namespace,
request.method.lower(),
request.url.path,
repr(sorted(request.query_params.items()))
])
@app.get("/")
@cache(expire=60, key_builder=request_key_builder)
async def index():
return dict(hello="world")By default, requests with a non-empty Authorization or Cookie header bypass the cache:
the endpoint runs normally and nothing is read from or written to the backend. Public
requests keep caching.
This is a conservative default. Session cookies are covered because they use the Cookie
header. Empty headers are treated as unauthenticated. Functions decorated without a
Request parameter have no HTTP identity context and keep using the cache.
To cache authenticated responses, opt in explicitly and provide a key builder that includes the identity:
import hashlib
import hmac
import os
from starlette.requests import Request
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
CACHE_KEY_SECRET = os.environ["CACHE_KEY_SECRET"].encode()
def identity_key_builder(
func,
namespace: str = "",
*,
request: Request = None,
response=None,
args,
kwargs,
):
identity = ""
if request is not None:
identity = "|".join(
[
request.headers.get("Authorization", ""),
request.headers.get("Cookie", ""),
request.headers.get("X-Tenant-Id", ""),
]
)
key_material = f"{identity}|{args}|{kwargs}"
digest = hmac.new(
CACHE_KEY_SECRET,
key_material.encode(),
hashlib.sha256,
).hexdigest()
return f"{namespace}:{func.__module__}:{func.__name__}:{digest}"
FastAPICache.init(backend, cache_authenticated=True, key_builder=identity_key_builder)
@app.get("/me")
@cache(expire=60)
async def me(request: Request):
...The key builder must include every dimension that affects the response: token, session, tenant, permissions, and any other validated principal state. Prefer the principal your app already validated over raw headers. Do not put credentials in plaintext in keys or logs; use HMAC-SHA256 with an application secret.
If cache_authenticated=True is used with the default key builder, an authenticated request
raises ValueError before the cache is accessed. The flag can also be set per endpoint:
@cache(cache_authenticated=True, key_builder=identity_key_builder).
The automatic bypass only detects Authorization and Cookie. If your app uses X-API-Key,
proxy-injected headers, request.state, or internal dependencies, review your cache strategy.
cache_authenticated=True does not make an unsafe key builder safe.
This protects the application cache. It does not configure CDN, reverse-proxy, or browser caching.
Upgrading from 0.3.0a1 or earlier: old entries written for authenticated requests may
remain under the default key. Clear or rotate the cache namespace before deploying, including
all instances/workers and persistent Redis caches. The bypass is not retroactive.
The InMemoryBackend stores cache data in memory and only deletes when an
expired key is accessed. This means that if you don't access a function after
data has been cached, the data will not be removed automatically.
expire=None means no expiration. Each backend instance owns its store and lock, so a new
instance does not see entries from another instance. Do not share one instance across event
loops or threads; use one instance per loop/thread. Subclasses that override __init__()
must call super().__init__(). InMemory is process-local; use Redis or Memcached for
cross-process sharing. expire=0 keeps the legacy fallback to the global expire value.
get_with_ttl returns -1 for no-expiration entries; the decorator omits max-age for
those entries and for expire=None.
When using the Redis backend, please make sure you pass in a redis client that does not decode responses (decode_responses must be False, which is the default). Cached data is stored as bytes (binary), decoding these in the Redis client would break caching.
coverage run -m pytest
coverage html
xdg-open htmlcov/index.htmlThis project is licensed under the Apache-2.0 License.