Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
prefix, JSON encoding and `default_ttl` as `set()`, and runs on the
backend's atomic `set_if_absent`, so of several concurrent callers exactly
one wins — for "send this webhook once" style deduplication. ([#65](https://github.com/allen0099/FastAPI-CacheX/issues/65))
- `add_routes(..., include_content_preview=False)` leaves response bodies out
of `/cached-records`: `content_preview` is `null`, while keys, sizes and expiry
are still reported. The default stays `True`. ([#79](https://github.com/allen0099/FastAPI-CacheX/issues/79))

## [0.3.6] - 2026-09-25

Expand Down
12 changes: 8 additions & 4 deletions docs/HTTP_CACHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,21 +248,25 @@ add_routes(
prefix="/admin/cache", # default "" -> /cached-hits, /cached-records
include_in_schema=False, # default: hidden from OpenAPI
dependencies=[Depends(verify_admin)],
include_content_preview=False, # default True: show the first 100 bytes
)
```

- `GET {prefix}/cached-hits` — every cached entry split into method, host, path
and query, with its ETag and expiry, plus counts of valid and expired entries
and the distinct cached paths. It does not count hits.
- `GET {prefix}/cached-records` — every cached record with its size, expiry and
a preview of the cached content.
a preview of the first 100 bytes of the cached content. With
`include_content_preview=False`, `content_preview` is `null` and no response
body leaves the server; keys, sizes and expiry are still reported.

> [!WARNING]
> **These routes have no authentication of their own.** `include_in_schema=False`
> only hides them from the OpenAPI document; anyone who guesses the path can read
> them. `/cached-records` includes a preview of the cached content and exposes
> your whole route structure. In production always pass
> `dependencies=[Depends(your_auth)]`, or mount them on an internal-only app.
> them. `/cached-records` includes a preview of the cached content (unless
> `include_content_preview=False`) and exposes your whole route structure. In
> production always pass `dependencies=[Depends(your_auth)]`, or mount them on
> an internal-only app.

> [!NOTE]
> The `ttl_remaining` field is not available on the Redis backend.
Expand Down
21 changes: 16 additions & 5 deletions fastapi_cachex/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class CachedRecord:
content_size: int
is_expired: bool
ttl_remaining: float | None
content_preview: str
content_preview: str | None


@dataclass
Expand Down Expand Up @@ -192,7 +192,9 @@ def _cached_hits(entries: list[_Entry]) -> CacheHitsResponse:
)


def _cached_records(entries: list[_Entry]) -> CachedRecordsResponse:
def _cached_records(
entries: list[_Entry], include_content_preview: bool
) -> CachedRecordsResponse:
cached_records = [
CachedRecord(
cache_key=e.cache_key,
Expand All @@ -205,8 +207,10 @@ def _cached_records(entries: list[_Entry]) -> CachedRecordsResponse:
content_size=len(e.entry.content),
is_expired=e.is_expired,
ttl_remaining=e.ttl_remaining,
content_preview=e.entry.content[:_PREVIEW_BYTES].decode(
"utf-8", errors="ignore"
content_preview=(
e.entry.content[:_PREVIEW_BYTES].decode("utf-8", errors="ignore")
if include_content_preview
else None
),
)
for e in entries
Expand All @@ -233,6 +237,7 @@ def add_routes(
prefix: str = "",
include_in_schema: bool = False,
dependencies: Sequence[Any] | None = None,
include_content_preview: bool = True,
) -> None:
"""Add cache monitoring routes to the FastAPI application.

Expand All @@ -250,6 +255,10 @@ def add_routes(
all monitoring routes. Useful for adding authentication
or authorization guards (e.g.
``[Depends(verify_api_key)]``).
include_content_preview: Whether ``/cached-records`` includes the first
bytes of each cached response body. When False,
``content_preview`` is ``null`` while keys, sizes and
expiry are still reported. Defaults to True.

Example:
from fastapi import FastAPI
Expand Down Expand Up @@ -292,4 +301,6 @@ async def get_cached_records() -> CachedRecordsResponse:
Returns:
CachedRecordsResponse containing cached records and statistics
"""
return _cached_records(_parse_entries(await _cache_data()))
return _cached_records(
_parse_entries(await _cache_data()), include_content_preview
)
23 changes: 23 additions & 0 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,29 @@ async def large_endpoint():
record = data["cached_records"][0]
assert len(record["content_preview"]) == 100

def test_cached_records_can_omit_content_preview(self, app, client, setup_cache):
"""include_content_preview=False hides bodies but keeps the metadata."""
add_routes(app, include_content_preview=False)

@app.get("/api/secret")
@cache(ttl=60)
async def get_secret():
return Response(
content=b'{"token": "s3cr3t"}', media_type="application/json"
)

client.get("/api/secret")

response = client.get("/cached-records")
assert response.status_code == 200
(record,) = response.json()["cached_records"]
assert record["content_preview"] is None
assert "s3cr3t" not in response.text
# Everything but the body is still reported.
assert record["path"] == "/api/secret"
assert record["content_size"] == len(b'{"token": "s3cr3t"}')
assert record["ttl_remaining"] is not None

def test_cached_records_summary_calculations(self, app, client, setup_cache):
"""Test that summary calculations are correct."""
add_routes(app)
Expand Down
Loading