From 79899ba8b3127b46e5fa5d3cd9ccbc12e8e0026a Mon Sep 17 00:00:00 2001 From: allen0099 Date: Tue, 29 Sep 2026 08:32:37 +0000 Subject: [PATCH] docs(http-cache): align HTTP caching docs and docstrings with the code --- docs/CACHE_FLOW.md | 54 ++++++++++++++++++++------------- docs/HTTP_CACHING.md | 11 +++++-- fastapi_cachex/cache.py | 7 +++-- fastapi_cachex/routes.py | 8 +++-- i18n/zh-TW/docs/CACHE_FLOW.md | 38 ++++++++++++++--------- i18n/zh-TW/docs/HTTP_CACHING.md | 8 +++-- 6 files changed, 79 insertions(+), 47 deletions(-) diff --git a/docs/CACHE_FLOW.md b/docs/CACHE_FLOW.md index 57271a2..105b4ca 100644 --- a/docs/CACHE_FLOW.md +++ b/docs/CACHE_FLOW.md @@ -13,25 +13,26 @@ HTTP request arrives @cache decorator intercepts it (only GET goes through the cache; every other method runs the handler directly and gets no Cache-Control header) ↓ -Build the cache key: method|||host|||path|||query_params - ↓ no-store? ── yes → run the handler, neither read nor write the cache, │ respond with Cache-Control: no-store ↓ no -private, or Authorization/session without public/cache_authorized? +private, no positive ttl, or Authorization/session without public/cache_authorized? ── yes → run the handler; compare If-None-Match to decide 304 or 200 - │ (the shared backend is neither read nor written; for - │ Authorization or a session, Cache-Control says private - │ instead of public) + │ (the shared backend is neither read nor written and the key + │ builder does not run; for Authorization or a session, + │ Cache-Control says private instead of public) ↓ no -Read the backend entry +Build the cache key: key_builder (default method|||host|||path|||query_params), +plus one name=value component per vary header + ↓ +Read the backend entry (with fail_open, a backend error counts as a miss) ↓ Request carries If-None-Match? ├─ and no-cache → run the handler first to compute the current ETag; match → 304 ├─ otherwise → compare with the cached entry's ETag; match → 304 with Age └─ no match / no header → continue ↓ -Cached entry exists, ttl is set, and no-cache is off? +Cached entry exists and no-cache is off? ├─ yes → respond with the cached content (including the stored status code │ and headers, plus Age; the handler does **not** run) └─ no → run the handler @@ -43,10 +44,13 @@ Cached entry exists, ttl is set, and no-cache is off? │ entry is left alone; a private/no-store header is kept) └─ regular response → set the ETag; write to the backend only if it differs from the existing entry's ETag + (with fail_open, a failed write is logged + and the response served unstored) ↓ Attach Cache-Control to the response (non-2xx responses are returned without it, a handler's own private/no-store Cache-Control is never replaced, and a -Set-Cookie response gets private instead of public) +Set-Cookie response gets private instead of public); with vary, add the +names to Vary on every GET response ``` ## Detailed steps @@ -96,9 +100,10 @@ and `X-Session-Token` a non-empty value is written as `sha256:`, so no token appears in the key. Query parameters are joined in the order the request sent them -(`str(request.query_params)`) and are **not sorted**, so `?page=1&limit=10` and -`?limit=10&page=1` are two separate cache entries. If you want them treated as -one, pass a custom `key_builder` that normalises the query string. +(`str(request.query_params)`) and are **not sorted** by default, so +`?page=1&limit=10` and `?limit=10&page=1` are two separate cache entries. To +treat them as one, set `@cache(sort_query=True)`, which orders the parameters +by name first (see [Cache keys](HTTP_CACHING.md#cache-keys)). The key format keeps each dimension cached independently: @@ -116,7 +121,7 @@ The decorator arguments control both the server-side behaviour and the ```python # Change how the server-side cache is used -@cache(no_cache=True) # Always re-run the handler (revalidate); entries are still written +@cache(no_cache=True) # Always re-run the handler (revalidate); entries are still written with a positive ttl @cache(no_store=True) # Never read or write the cache # Normal caching behaviour @@ -133,8 +138,11 @@ The decorator arguments control both the server-side behaviour and the ``` Arguments are validated when the decorator is applied, and a `CacheXError` is -raised if `public` and `private` are both set, or if only one of `stale` / -`stale_ttl` is given. +raised if `public` and `private` are both set, if only one of `stale` / +`stale_ttl` is given, if `ttl` is not an `int`, is negative or is larger than +`MAX_TTL`, if `vary` is not a list of header field names, if `sort_query` is +not a `bool` or is combined with a custom `key_builder`, or if `key_builder` is +an `async` callable. The header value is built once per decorated route: @@ -208,11 +216,14 @@ if request.method != "GET": if no_store: return await render() # no read, no write -authorized = "authorization" in request.headers and not (public or cache_authorized) -if private or not ttl or authorized: +bypass = private or not ttl +# Authorization header, a session the middleware loaded, or non-empty request.session +credential = None if bypass or public or cache_authorized else request_credential(request) +if bypass or credential: response, etag = await render() # backend neither read nor written return not_modified(...) if etag_matches(client_etag, etag) else response +cache_key = key_builder(request) + vary_components(request) # built only here entry = await backend.get(cache_key) # expired entries are already skipped here if client_etag and no_cache: @@ -475,6 +486,7 @@ class CacheEntry: media_type: str | None = None status_code: int = 200 # replayed as-is headers: dict[str, str] | None = None # sent back on replay + stored_at: float | None = None # epoch seconds when @cache stored it; drives Age @dataclass @@ -486,7 +498,7 @@ class CacheItem: `headers` stores the headers the handler set itself, excluding fields that must be recomputed for every response or must not be replayed: `Set-Cookie`, `Content-Length`, `Transfer-Encoding`, `Connection`, `Date`, `ETag`, -`Cache-Control` and `Content-Type` (`Content-Type` is restored from +`Cache-Control`, `Content-Type` and `Age` (`Content-Type` is restored from `media_type`; storing both would emit the header twice). Counters (`backend.increment()`) are also represented as a `CacheEntry`: the @@ -525,9 +537,9 @@ without reading or writing the cache and without adding a `Cache-Control` header. **Q: Why are there several cache entries for the same endpoint?** -A: Because the cache key includes the query parameters, and they are **not -sorted**. `/users?page=1` and `/users?page=2` are different entries, and so are -`?a=1&b=2` and `?b=2&a=1`. +A: Because the cache key includes the query parameters, and by default they are +**not sorted**. `/users?page=1` and `/users?page=2` are different entries, and +so are `?a=1&b=2` and `?b=2&a=1` unless the route sets `sort_query=True`. **Q: How does MemoryBackend work across multiple processes?** A: It doesn't. Each process has its own cache; use Redis in production. diff --git a/docs/HTTP_CACHING.md b/docs/HTTP_CACHING.md index 7399fac..9fcea31 100644 --- a/docs/HTTP_CACHING.md +++ b/docs/HTTP_CACHING.md @@ -139,7 +139,9 @@ A response that belongs to one caller is never stored either (#296): caller's identity (see [Authenticated endpoints](#authenticated-endpoints)). `must_revalidate=True` does not lift the bypass: RFC 9111 would let a shared cache reuse such a response under `must-revalidate`, but the library - requires an explicit opt-in. + requires an explicit opt-in. Routes that skip the backend anyway + (`private=True`, or no positive `ttl`) do not check for credentials and + send their own `Cache-Control` unchanged. A request has a session when `FastAPICacheXSessionMiddleware` (or the deprecated `SessionMiddleware`) loaded one for it, from the token header, a bearer token or the session cookie, with or without a user, or when @@ -398,7 +400,7 @@ lower-cased, the value trimmed (repeated header lines joined with `,`), and a missing header treated as an empty one. The components are escaped like the rest of the key and come after whatever the `key_builder` returns, so `vary` and a custom key builder compose: -`GET|||example.com|||/greeting|||||||tenant-1|||accept-language=de` for +`GET|||example.com|||/greeting||||||tenant-1|||accept-language=de` for `key_builder` returning `build_cache_key(request, "tenant-1")`. Routes without `vary` keep their keys. @@ -424,7 +426,7 @@ holds the full hex SHA-256 of the value (trimmed and joined as above) instead of the value: ``` -GET|||example.com|||/me|||||||authorization=sha256:3f0a…(64 hex digits) +GET|||example.com|||/me||||||authorization=sha256:3f0a…(64 hex digits) ``` The same token always gives the same digest, so it hits its own entry, and two @@ -710,6 +712,9 @@ add_routes( `content_type` is always `"bytes"` and is kept for compatibility; read `media_type` instead. +Both routes list only route entries (keys in the `method|||host|||path|||query` +format); `CacheManager`, session, state and lock keys are skipped. + > [!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 diff --git a/fastapi_cachex/cache.py b/fastapi_cachex/cache.py index 134aa95..99d90f0 100644 --- a/fastapi_cachex/cache.py +++ b/fastapi_cachex/cache.py @@ -1020,6 +1020,9 @@ def cache( backend as ``private=True`` does (RFC 9111 §3.5), unless ``public`` is set, and its response is sent with ``private``. A token that does not resolve to a session does not count. + Routes that skip the backend anyway (``private=True``, or no + positive ``ttl``) do not check for credentials, so their + ``Cache-Control`` is sent unchanged. The first such bypass is logged at ``WARNING`` once per route and credential kind (the route template and the kind, never the value), since it otherwise leaves the route with no cache hits. @@ -1340,8 +1343,8 @@ async def serve(*args: Any, **kwargs: Any) -> Response: _age_headers(cached_data, ttl), ) - # If we don't have If-None-Match header, check if we have a valid cached copy - # and can serve it directly (cache hit without ETag comparison) + # No 304 was sent (no If-None-Match, or it did not match): serve a + # valid cached copy directly (cache hit without running the handler) if cached_data and not no_cache: logger.debug("Cache HIT (TTL valid); key=%s", cache_key) return Response( diff --git a/fastapi_cachex/routes.py b/fastapi_cachex/routes.py index e2be483..2ded024 100644 --- a/fastapi_cachex/routes.py +++ b/fastapi_cachex/routes.py @@ -341,7 +341,8 @@ def add_routes( async def get_cached_hits() -> CacheHitsResponse: """List the cached route entries. - Splits every cached key into method, host, path, query and any + Splits every cached route key (other keys are skipped) into + method, host, path, query and any extra components a key builder appended, with its ETag and expiry, plus counts of valid and expired entries and the distinct cached paths. Cache hits are not counted. @@ -359,8 +360,9 @@ async def get_cached_hits() -> CacheHitsResponse: async def get_cached_records() -> CachedRecordsResponse: """Display currently cached records. - Returns all currently cached records in the cache backend with their - content information and expiry details. + Returns every route entry in the cache backend (keys in the + ``method|||host|||path|||query`` format; other keys are skipped) with + its content information and expiry details. Returns: CachedRecordsResponse containing cached records and statistics diff --git a/i18n/zh-TW/docs/CACHE_FLOW.md b/i18n/zh-TW/docs/CACHE_FLOW.md index 9fa4fbd..0487734 100644 --- a/i18n/zh-TW/docs/CACHE_FLOW.md +++ b/i18n/zh-TW/docs/CACHE_FLOW.md @@ -10,24 +10,25 @@ HTTP 請求抵達 @cache 裝飾器攔截請求(只有 GET 會經過快取;其他所有 方法直接執行 handler,也不會帶上 Cache-Control 標頭) ↓ -建立快取鍵:method|||host|||path|||query_params - ↓ no-store? ── 是 → 執行 handler,既不讀取也不寫入快取, │ 回應帶上 Cache-Control: no-store ↓ 否 -private,或帶有 Authorization/Session 且未設定 public/cache_authorized? +private、沒有正數的 ttl,或帶有 Authorization/Session 且未設定 public/cache_authorized? ── 是 → 執行 handler;比對 If-None-Match 決定回傳 304 或 200 - │ (共用後端既不讀取也不寫入;Authorization 或 Session 的情況下 - │ Cache-Control 以 private 取代 public) + │ (共用後端既不讀取也不寫入,key builder 也不會執行; + │ Authorization 或 Session 的情況下 Cache-Control 以 private 取代 public) ↓ 否 -讀取後端項目 +建立快取鍵:key_builder(預設為 method|||host|||path|||query_params), +再為每個 vary 標頭附加一個 name=value 段 + ↓ +讀取後端項目(fail_open 時,後端錯誤視為未命中) ↓ 請求帶有 If-None-Match? ├─ 且為 no-cache → 先執行 handler 計算目前的 ETag;相符 → 304 ├─ 其他情況 → 與快取項目的 ETag 比對;相符 → 帶 Age 的 304 └─ 不相符/沒有此標頭 → 繼續 ↓ -快取項目存在、已設定 ttl,且未啟用 no-cache? +快取項目存在,且未啟用 no-cache? ├─ 是 → 以快取內容回應(包含儲存的狀態碼 │ 與標頭,並加上 Age;handler **不會**執行) └─ 否 → 執行 handler @@ -39,10 +40,13 @@ private,或帶有 Authorization/Session 且未設定 public/cache_authoriz │ private/no-store 標頭保留原樣) └─ 一般回應 → 設定 ETag;只有與既有項目的 ETag 不同時才寫入後端 + (fail_open 時,寫入失敗會記錄警告, + 回應照常送出但不儲存) ↓ 在回應中附加 Cache-Control(非 2xx 回應回傳時不帶此標頭, handler 自己送出的 private/no-store Cache-Control 永遠不會被取代, -設定 Set-Cookie 的回應則以 private 取代 public) +設定 Set-Cookie 的回應則以 private 取代 public);設定 vary 時, +會把這些名稱加入每個 GET 回應的 Vary ``` ## 詳細步驟 {#detailed-steps} @@ -76,7 +80,7 @@ host 與路徑會先經過百分比編碼:`|` 變成 `%7C`,`%` 變成 `%25` 自訂的 `key_builder` 可以用 `build_cache_key(request, *components)` 在查詢字串之後加入其他段;這些段以同樣方式編碼,`clear_path()` 也仍會比對路徑(見 [HTTP 快取](HTTP_CACHING.md#adding-components-to-the-key)中的「在鍵中加入其他段」)。`@cache(vary=[...])` 會在 key builder 回傳的鍵之後,為每個列出的請求標頭附加一個 `name=value` 段,並把這些名稱加入回應的 `Vary` 標頭(見 [HTTP 快取](HTTP_CACHING.md#varying-on-request-headers)中的「依請求標頭區分」)。對於憑證標頭 `Authorization`、`Proxy-Authorization`、`Cookie` 與 `X-Session-Token`,非空的值會寫成 `sha256:<十六進位摘要>`,因此鍵中不會出現任何權杖。 -查詢參數依請求送出的順序串接(`str(request.query_params)`),**不會排序**,因此 `?page=1&limit=10` 與 `?limit=10&page=1` 是兩個不同的快取項目。若希望兩者視為同一個,請傳入自訂的 `key_builder` 將查詢字串正規化。 +查詢參數依請求送出的順序串接(`str(request.query_params)`),預設**不會排序**,因此 `?page=1&limit=10` 與 `?limit=10&page=1` 是兩個不同的快取項目。若希望兩者視為同一個,請設定 `@cache(sort_query=True)`,先依名稱排序參數(見 [HTTP 快取](HTTP_CACHING.md#cache-keys)中的「快取鍵」)。 這個快取鍵格式讓每個維度各自獨立快取: @@ -91,7 +95,7 @@ host 與路徑會先經過百分比編碼:`|` 變成 `%7C`,`%` 變成 `%25` ```python # 改變伺服器端快取的使用方式 -@cache(no_cache=True) # 每次都重新執行 handler(重新驗證);項目仍會寫入 +@cache(no_cache=True) # 每次都重新執行 handler(重新驗證);ttl 為正數時項目仍會寫入 @cache(no_store=True) # 永不讀取或寫入快取 # 一般快取行為 @@ -107,7 +111,7 @@ host 與路徑會先經過百分比編碼:`|` 變成 `%7C`,`%` 變成 `%25` @cache(ttl=60, stale="error", stale_ttl=300) # stale-if-error=300 ``` -參數會在套用裝飾器時驗證;若同時設定 `public` 與 `private`,或只提供 `stale`/`stale_ttl` 其中之一,會拋出 `CacheXError`。 +參數會在套用裝飾器時驗證;若同時設定 `public` 與 `private`、只提供 `stale`/`stale_ttl` 其中之一、`ttl` 不是 `int`、為負數或大於 `MAX_TTL`、`vary` 不是由標頭欄位名稱組成的 list、`sort_query` 不是 `bool` 或與自訂的 `key_builder` 一起使用,或 `key_builder` 是 `async` 可呼叫物件,會拋出 `CacheXError`。 標頭值在每個被裝飾的路由上只建立一次: @@ -160,11 +164,14 @@ if request.method != "GET": if no_store: return await render() # 不讀取,不寫入 -authorized = "authorization" in request.headers and not (public or cache_authorized) -if private or not ttl or authorized: +bypass = private or not ttl +# Authorization 標頭、中介軟體載入的 Session,或不是空的 request.session +credential = None if bypass or public or cache_authorized else request_credential(request) +if bypass or credential: response, etag = await render() # 既不讀取也不寫入後端 return not_modified(...) if etag_matches(client_etag, etag) else response +cache_key = key_builder(request) + vary_components(request) # 只在這裡建立 entry = await backend.get(cache_key) # 過期的項目已在此略過 if client_etag and no_cache: @@ -380,6 +387,7 @@ class CacheEntry: media_type: str | None = None status_code: int = 200 # 原樣重播 headers: dict[str, str] | None = None # 重播時送回 + stored_at: float | None = None # @cache 儲存它時的 epoch 秒數;用來計算 Age @dataclass @@ -388,7 +396,7 @@ class CacheItem: expiry: float | None = None # epoch 秒數;僅 MemoryBackend 使用 ``` -`headers` 儲存 handler 自行設定的標頭,但排除每次回應都必須重新計算或不得重播的欄位:`Set-Cookie`、`Content-Length`、`Transfer-Encoding`、`Connection`、`Date`、`ETag`、`Cache-Control` 與 `Content-Type`(`Content-Type` 會由 `media_type` 還原;兩者都儲存會使該標頭送出兩次)。 +`headers` 儲存 handler 自行設定的標頭,但排除每次回應都必須重新計算或不得重播的欄位:`Set-Cookie`、`Content-Length`、`Transfer-Encoding`、`Connection`、`Date`、`ETag`、`Cache-Control`、`Content-Type` 與 `Age`(`Content-Type` 會由 `media_type` 還原;兩者都儲存會使該標頭送出兩次)。 計數器(`backend.increment()`)同樣以 `CacheEntry` 表示:fingerprint 一律為 `counter`,`content` 為十進位數值的位元組,因此刪除、清除與監控都以相同方式處理它們。 @@ -412,7 +420,7 @@ handler 不必自行宣告 `Request`:`@cache` 會在函式簽名中注入一 **Q:為什麼 POST/PUT 的回應不會被快取?** A:`@cache` 只適用於 GET。其他所有方法都直接執行 handler,不讀取或寫入快取,也不會加上 `Cache-Control` 標頭。 -**Q:為什麼同一個端點有好幾個快取項目?** A:因為快取鍵包含查詢參數,而且查詢參數**不會排序**。`/users?page=1` 與 `/users?page=2` 是不同的項目,`?a=1&b=2` 與 `?b=2&a=1` 也是。 +**Q:為什麼同一個端點有好幾個快取項目?** A:因為快取鍵包含查詢參數,而且查詢參數預設**不會排序**。`/users?page=1` 與 `/users?page=2` 是不同的項目;除非路由設定了 `sort_query=True`,`?a=1&b=2` 與 `?b=2&a=1` 也是。 **Q:MemoryBackend 在多個行程下如何運作?** A:無法運作。每個行程都有自己的快取;正式環境請使用 Redis。 diff --git a/i18n/zh-TW/docs/HTTP_CACHING.md b/i18n/zh-TW/docs/HTTP_CACHING.md index f70f49b..fb58f07 100644 --- a/i18n/zh-TW/docs/HTTP_CACHING.md +++ b/i18n/zh-TW/docs/HTTP_CACHING.md @@ -87,7 +87,7 @@ GET /items → 200, Cache-Control: max-age=60, Age: 42(儲存後 42 秒送出 屬於單一呼叫者的回應同樣不會被儲存(#296): -- **請求帶有 `Authorization` 或 Session。** 依照 RFC 9111 §3.5 對共用快取的要求,這類請求會像 `private=True` 一樣繞過後端:不讀取也不寫入,handler 照常執行,`If-None-Match` 與新產生的回應比對。回應(以及 304)會以 `private` 取代 `public` 送出,並保留裝飾器的其他指令(`no_cache` 路由則為 `private, no-cache`),讓 CDN 或代理也不會儲存它。`public=True` 的路由不受此限,設定 `cache_authorized=True` 的路由也一樣;後者是給包含呼叫者身分的 key builder 使用的明確選項(見[需驗證身分的端點](#authenticated-endpoints))。`must_revalidate=True` 不會解除繞過:RFC 9111 允許共用快取在 `must-revalidate` 下重複使用這類回應,但本函式庫要求明確選擇啟用。請求「帶有 Session」是指 `FastAPICacheXSessionMiddleware`(或已棄用的 `SessionMiddleware`)為它載入了 Session(權杖來自標頭、Bearer 權杖或 Session Cookie 皆可,有沒有使用者都算),或在任何 Session 中介軟體(包括 Starlette 的)下 `request.session` 不是空的。解析不出 Session 的權杖(偽造、過期)不算,因此無法用來略過快取。0.3.9 以前只有 `Authorization` 會觸發繞過,讀取 Session 的路由只加上 `@cache` 時,會把一位訪客的回應提供給下一位(#319)。每個路由第一次繞過時會以 `WARNING` 等級記錄(見[帶有憑證的請求](#requests-with-credentials))。 +- **請求帶有 `Authorization` 或 Session。** 依照 RFC 9111 §3.5 對共用快取的要求,這類請求會像 `private=True` 一樣繞過後端:不讀取也不寫入,handler 照常執行,`If-None-Match` 與新產生的回應比對。回應(以及 304)會以 `private` 取代 `public` 送出,並保留裝飾器的其他指令(`no_cache` 路由則為 `private, no-cache`),讓 CDN 或代理也不會儲存它。`public=True` 的路由不受此限,設定 `cache_authorized=True` 的路由也一樣;後者是給包含呼叫者身分的 key builder 使用的明確選項(見[需驗證身分的端點](#authenticated-endpoints))。`must_revalidate=True` 不會解除繞過:RFC 9111 允許共用快取在 `must-revalidate` 下重複使用這類回應,但本函式庫要求明確選擇啟用。本來就不經過後端的路由(`private=True`,或沒有正數的 `ttl`)不會檢查憑證,會原樣送出自己的 `Cache-Control`。請求「帶有 Session」是指 `FastAPICacheXSessionMiddleware`(或已棄用的 `SessionMiddleware`)為它載入了 Session(權杖來自標頭、Bearer 權杖或 Session Cookie 皆可,有沒有使用者都算),或在任何 Session 中介軟體(包括 Starlette 的)下 `request.session` 不是空的。解析不出 Session 的權杖(偽造、過期)不算,因此無法用來略過快取。0.3.9 以前只有 `Authorization` 會觸發繞過,讀取 Session 的路由只加上 `@cache` 時,會把一位訪客的回應提供給下一位(#319)。每個路由第一次繞過時會以 `WARNING` 等級記錄(見[帶有憑證的請求](#requests-with-credentials))。 - **handler 自己的 `Cache-Control` 含有 `private` 或 `no-store`**(完整指令,不分大小寫)。回應照常送出但不儲存,而且 handler 的標頭會原樣送出,不會被裝飾器的標頭取代。 - **回應設定了 cookie。** 回應照常送出(包含 `Set-Cookie`),但不儲存;它(以及 304)會以 `private` 取代 `public` 送出並保留其他指令,讓下游的共用快取也不會儲存它。 @@ -231,7 +231,7 @@ async def greeting(request: Request): return {"text": translate("hello", request.headers.get("accept-language"))} ``` -每個列出的標頭都會在鍵中加入一個 `name=value` 段:名稱轉為小寫,值去除前後空白(重複的標頭行以 `,` 串接),缺少的標頭視同空值。這些段與鍵的其他部分一樣經過編碼,並接在 `key_builder` 回傳的鍵之後,因此 `vary` 可以與自訂的 key builder 一起使用:`key_builder` 回傳 `build_cache_key(request, "tenant-1")` 時,鍵為 `GET|||example.com|||/greeting|||||||tenant-1|||accept-language=de`。沒有設定 `vary` 的路由,鍵維持不變。 +每個列出的標頭都會在鍵中加入一個 `name=value` 段:名稱轉為小寫,值去除前後空白(重複的標頭行以 `,` 串接),缺少的標頭視同空值。這些段與鍵的其他部分一樣經過編碼,並接在 `key_builder` 回傳的鍵之後,因此 `vary` 可以與自訂的 key builder 一起使用:`key_builder` 回傳 `build_cache_key(request, "tenant-1")` 時,鍵為 `GET|||example.com|||/greeting||||||tenant-1|||accept-language=de`。沒有設定 `vary` 的路由,鍵維持不變。 這些名稱也會加入該路由對 GET 請求的每個回應的 `Vary` 標頭,不論是 200 或 304,也不論是否由後端提供(`private`、`no_store`、繞過後端的 `Authorization` 請求,或未儲存的回應),讓應用程式前方的共用快取也依它們區分。回應已列出的名稱(不分大小寫)不會重複加入,帶有 `Vary: *` 的回應則維持原樣。 @@ -242,7 +242,7 @@ async def greeting(request: Request): 快取鍵並非機密:`get_all_keys()` 會列出它、`/cached-records` 與 `/cached-hits` 監控路由會顯示它,Redis 或 Memcached 的鍵空間也會原樣儲存它。因此對於攜帶憑證的標頭,也就是 `Authorization`、`Proxy-Authorization`、`Cookie` 與 `X-Session-Token`(Session 子系統預設的 `header_name`),不分大小寫,該段存放的是值(依上述方式去除空白並串接)的完整十六進位 SHA-256,而不是值本身: ``` -GET|||example.com|||/me|||||||authorization=sha256:3f0a…(64 個十六進位字元) +GET|||example.com|||/me||||||authorization=sha256:3f0a…(64 個十六進位字元) ``` 同一個權杖永遠得到同一個摘要,因此會命中自己的項目;兩個不同的權杖則得到兩筆項目。缺少或空白的憑證標頭不會雜湊,而是與其他空標頭一樣維持 `authorization=`,讓所有匿名呼叫者共用一筆項目,鍵也仍看得出這是匿名的那一筆。其他標頭(包括以其他名稱設定的 Session 標頭)都維持可讀;若你的標頭帶有機密,請透過 `key_builder`(自行雜湊)而不是 `vary` 以它作為鍵。 @@ -435,6 +435,8 @@ add_routes( - `GET {prefix}/cached-hits`:列出每筆快取項目,拆分為方法、主機、路徑與查詢,附上 ETag 與到期時間,另外統計有效與已過期的項目數,以及不重複的快取路徑。它不會計算命中次數。 - `GET {prefix}/cached-records`:列出每筆快取紀錄的大小、到期時間、`media_type`(儲存的回應的媒體類型,沒有時為 `null`),以及快取內容前 100 個位元組的預覽。設定 `include_content_preview=False` 時,`content_preview` 為 `null`,不會有任何回應本文離開伺服器;鍵、大小與到期時間仍會回報。`content_type` 一律是 `"bytes"`,只為相容而保留;請改讀 `media_type`。 +兩個路由都只列出路由項目(格式為 `method|||host|||path|||query` 的鍵);`CacheManager`、Session、state 與鎖的鍵都會略過。 + > [!WARNING] > **這些路由本身沒有任何身分驗證。** `include_in_schema=False` 只是讓它們不出現在 OpenAPI 文件中;任何猜到路徑的人都能讀取。`/cached-records` 含有快取內容的預覽(除非設定 `include_content_preview=False`),並會暴露整個路由結構。正式環境中請務必傳入 `dependencies=[Depends(your_auth)]`,或將它們掛載在僅供內部使用的應用程式上。 >