Add a cache status header to the response

The header name is configurable, and defaults to `X-FastAPI-Cache`,
the value is either `HIT` or `MISS`.

Note that the header is not set at all when the cache is disabled.
This commit is contained in:
Martijn Pieters
2023-05-12 14:12:00 +01:00
committed by Martijn Pieters
parent 29426de95f
commit 915f3dd8f2
4 changed files with 28 additions and 3 deletions

View File

@@ -99,6 +99,7 @@ namespace | str, namespace to use to store certain cache items
coder | which coder to use, e.g. JsonCoder coder | which coder to use, e.g. JsonCoder
key_builder | which key builder to use, default to builtin key_builder | which key builder to use, default to builtin
injected_dependency_namespace | prefix for injected dependency keywords, defaults to `__fastapi_cache`. injected_dependency_namespace | prefix for injected dependency keywords, defaults to `__fastapi_cache`.
cache_status_header | Name for the header on the response indicating if the request was served from cache; either `HIT` or `MISS`. Defaults to `X-FastAPI-Cache`.
You can also use `cache` as decorator like other cache tools to cache common function result. You can also use `cache` as decorator like other cache tools to cache common function result.

View File

@@ -22,6 +22,7 @@ class FastAPICache:
_init: ClassVar[bool] = False _init: ClassVar[bool] = False
_coder: ClassVar[Optional[Type[Coder]]] = None _coder: ClassVar[Optional[Type[Coder]]] = None
_key_builder: ClassVar[Optional[KeyBuilder]] = None _key_builder: ClassVar[Optional[KeyBuilder]] = None
_cache_status_header: ClassVar[Optional[str]] = None
_enable: ClassVar[bool] = True _enable: ClassVar[bool] = True
@classmethod @classmethod
@@ -32,6 +33,7 @@ class FastAPICache:
expire: Optional[int] = None, expire: Optional[int] = None,
coder: Type[Coder] = JsonCoder, coder: Type[Coder] = JsonCoder,
key_builder: KeyBuilder = default_key_builder, key_builder: KeyBuilder = default_key_builder,
cache_status_header: str = "X-FastAPI-Cache",
enable: bool = True, enable: bool = True,
) -> None: ) -> None:
if cls._init: if cls._init:
@@ -42,6 +44,7 @@ class FastAPICache:
cls._expire = expire cls._expire = expire
cls._coder = coder cls._coder = coder
cls._key_builder = key_builder cls._key_builder = key_builder
cls._cache_status_header = cache_status_header
cls._enable = enable cls._enable = enable
@classmethod @classmethod
@@ -52,6 +55,7 @@ class FastAPICache:
cls._expire = None cls._expire = None
cls._coder = None cls._coder = None
cls._key_builder = None cls._key_builder = None
cls._cache_status_header = None
cls._enable = True cls._enable = True
@classmethod @classmethod
@@ -78,6 +82,11 @@ class FastAPICache:
assert cls._key_builder, "You must call init first!" # nosec: B101 assert cls._key_builder, "You must call init first!" # nosec: B101
return cls._key_builder return cls._key_builder
@classmethod
def get_cache_status_header(cls) -> str:
assert cls._cache_status_header, "You must call init first!" # nosec: B101
return cls._cache_status_header
@classmethod @classmethod
def get_enable(cls) -> bool: def get_enable(cls) -> bool:
return cls._enable return cls._enable

View File

@@ -144,6 +144,7 @@ def cache(
expire = expire or FastAPICache.get_expire() expire = expire or FastAPICache.get_expire()
key_builder = key_builder or FastAPICache.get_key_builder() key_builder = key_builder or FastAPICache.get_key_builder()
backend = FastAPICache.get_backend() backend = FastAPICache.get_backend()
cache_status_header = FastAPICache.get_cache_status_header()
cache_key = key_builder( cache_key = key_builder(
func, func,
@@ -181,6 +182,7 @@ def cache(
{ {
"Cache-Control": f"max-age={expire}", "Cache-Control": f"max-age={expire}",
"ETag": f"W/{hash(to_cache)}", "ETag": f"W/{hash(to_cache)}",
cache_status_header: "MISS",
} }
) )
@@ -191,6 +193,7 @@ def cache(
{ {
"Cache-Control": f"max-age={ttl}", "Cache-Control": f"max-age={ttl}",
"ETag": etag, "ETag": etag,
cache_status_header: "HIT",
} }
) )

View File

@@ -20,15 +20,18 @@ def init_cache() -> Generator[Any, Any, None]:
def test_datetime() -> None: def test_datetime() -> None:
with TestClient(app) as client: with TestClient(app) as client:
response = client.get("/datetime") response = client.get("/datetime")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
now = response.json().get("now") now = response.json().get("now")
now_ = pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call] now_ = pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined] assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
response = client.get("/datetime") response = client.get("/datetime")
assert response.headers.get("X-FastAPI-Cache") == "HIT"
now = response.json().get("now") now = response.json().get("now")
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined] assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
time.sleep(3) time.sleep(3)
response = client.get("/datetime") response = client.get("/datetime")
now = response.json().get("now") now = response.json().get("now")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
now = pendulum.parse(now).replace(microsecond=0) # type: ignore[attr-defined] now = pendulum.parse(now).replace(microsecond=0) # type: ignore[attr-defined]
assert now != now_ assert now != now_
assert now == pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call] assert now == pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
@@ -38,15 +41,18 @@ def test_date() -> None:
"""Test path function without request or response arguments.""" """Test path function without request or response arguments."""
with TestClient(app) as client: with TestClient(app) as client:
response = client.get("/date") response = client.get("/date")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined] assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
# do it again to test cache # do it again to test cache
response = client.get("/date") response = client.get("/date")
assert response.headers.get("X-FastAPI-Cache") == "HIT"
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined] assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
# now test with cache disabled, as that's a separate code path # now test with cache disabled, as that's a separate code path
FastAPICache._enable = False # pyright: ignore[reportPrivateUsage] FastAPICache._enable = False # pyright: ignore[reportPrivateUsage]
response = client.get("/date") response = client.get("/date")
assert "X-FastAPI-Cache" not in response.headers
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined] assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
FastAPICache._enable = True # pyright: ignore[reportPrivateUsage] FastAPICache._enable = True # pyright: ignore[reportPrivateUsage]
@@ -72,6 +78,7 @@ def test_kwargs() -> None:
with TestClient(app) as client: with TestClient(app) as client:
name = "Jon" name = "Jon"
response = client.get("/kwargs", params={"name": name}) response = client.get("/kwargs", params={"name": name})
assert "X-FastAPI-Cache" not in response.headers
assert response.json() == {"name": name} assert response.json() == {"name": name}
@@ -83,20 +90,25 @@ def test_method() -> None:
def test_pydantic_model() -> None: def test_pydantic_model() -> None:
with TestClient(app) as client: with TestClient(app) as client:
r1 = client.get("/pydantic_instance").json() r1 = client.get("/pydantic_instance")
r2 = client.get("/pydantic_instance").json() assert r1.headers.get("X-FastAPI-Cache") == "MISS"
assert r1 == r2 r2 = client.get("/pydantic_instance")
assert r2.headers.get("X-FastAPI-Cache") == "HIT"
assert r1.json() == r2.json()
def test_non_get() -> None: def test_non_get() -> None:
with TestClient(app) as client: with TestClient(app) as client:
response = client.put("/uncached_put") response = client.put("/uncached_put")
assert "X-FastAPI-Cache" not in response.headers
assert response.json() == {"value": 1} assert response.json() == {"value": 1}
response = client.put("/uncached_put") response = client.put("/uncached_put")
assert "X-FastAPI-Cache" not in response.headers
assert response.json() == {"value": 2} assert response.json() == {"value": 2}
def test_alternate_injected_namespace() -> None: def test_alternate_injected_namespace() -> None:
with TestClient(app) as client: with TestClient(app) as client:
response = client.get("/namespaced_injection") response = client.get("/namespaced_injection")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
assert response.json() == {"__fastapi_cache_request": 42, "__fastapi_cache_response": 17} assert response.json() == {"__fastapi_cache_request": 42, "__fastapi_cache_response": 17}