diff --git a/docs/HTTP_CACHING.md b/docs/HTTP_CACHING.md index d6d2a05..ae4008e 100644 --- a/docs/HTTP_CACHING.md +++ b/docs/HTTP_CACHING.md @@ -316,7 +316,10 @@ exactly as in the unsorted key, so only the order changes: a query already in order gets the same key with or without the flag. What the key already treats as equal stays equal (`?a` and `?a=`, an empty segment from `&&`), and nothing else is merged. The default is `False`, which leaves every existing key -unchanged. +unchanged. 0.4.0 makes `True` the default, together with its other cache key +changes ([#72](https://github.com/allen0099/FastAPI-CacheX/issues/72), see +[Migrating to 0.4.0](MIGRATING_0_4.md#cache-keys)). A handler that depends on the +query order as sent can keep `sort_query=False`. `sort_query` applies to the default key builder only. Combined with a custom `key_builder` it raises `CacheXError` when the decorator is applied; call diff --git a/docs/MIGRATING_0_4.md b/docs/MIGRATING_0_4.md index d412568..e823c67 100644 --- a/docs/MIGRATING_0_4.md +++ b/docs/MIGRATING_0_4.md @@ -86,16 +86,16 @@ config = SessionConfig( ### Login and logout {#login-logout} -0.4.0 makes becoming authenticated go through one explicit API that always issues a new session ID ([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256)). Known so far: +0.4.0 makes becoming authenticated go through one explicit API that always issues a new session ID ([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256)): - `login(request, user)` (in 0.3.9 already, `from fastapi_cachex.session import login`) attaches the user and rotates the ID. Use it today instead of setting `session.user` yourself. -- A logout API is added; `request.session.clear()` keeps meaning logout. -- `Session.user` becomes read-only outside these calls. Code that assigns it directly breaks. -- A login starts a new session instead of promoting the anonymous one. Which data is carried over (all by default, or a `keep=` list) is not decided yet. -- For a few seconds after a rotation the old ID resolves to the new session, so in-flight requests carrying the old token do not fail. -- `rotate_session_id()` stays for privilege changes without a new user, possibly renamed. +- `await logout(request)` is added. It deletes the session, and a cookie client gets its cookie expired. `request.session.clear()` keeps meaning logout. +- `Session.user` becomes read-only outside `login()` and `SessionManager.create_session(user=...)`. Code that assigns it directly breaks: under the middleware, use `login()`; without it, create the session with `create_session(user=...)`. +- A login carries the anonymous session's data over by default, so a cart survives it. An optional `keep=` argument narrows that (`keep=["cart"]`, or `keep=[]` for nothing). +- The old ID stops resolving the moment `login()` or `rotate_session_id()` rotates it, with no grace period: during one, a planted token would resolve to the logged-in session. +- `rotate_session_id()` keeps its name, for privilege changes without a new user. -The exact signatures are not final, so 0.3.9 does not warn about them. An application that decides "logged in" from its own `request.session` keys (`request.session.get("user_id")`) is outside what the library can see; use the library's identity (`session.user`, `AuthenticatedSession`) or rotate the ID yourself. +Most of this is new API, and nothing in 0.3.x can tell which code assigns `session.user`, so 0.3.9 does not warn. An application that decides "logged in" from its own `request.session` keys (`request.session.get("user_id")`) is outside what the library can see; use the library's identity (`session.user`, `AuthenticatedSession`) or rotate the ID yourself. Before: @@ -182,7 +182,12 @@ SessionConfig( ### Session writes {#session-writes} -0.4.0 writes sessions conditionally ([#128](https://github.com/allen0099/FastAPI-CacheX/issues/128)): a request that loaded a session before another request deleted, invalidated or rotated it can no longer bring the record back when it saves. No code change is needed. A custom backend has to support the write-if-present primitive this adds; its shape is not decided yet. +0.4.0 writes sessions conditionally ([#128](https://github.com/allen0099/FastAPI-CacheX/issues/128)): a request that loaded a session before another request deleted, invalidated or rotated it can no longer bring the record back when it saves. No code change is needed. + +- Ordinary saves become conditional: the middleware's save of `request.session` changes, sliding renewal and `update_session()`. Each succeeds only while the stored record still equals what this request last read or wrote. Deleting, invalidating, expiring and rotating stay unconditional, so a security action always wins. +- A rejected save is dropped and logged. The response is still sent, without a session token. +- **Side effect:** when two requests change the same session at the same time (two tabs adding to a cart), the first save wins and the second is dropped. Today the last save wins, so one of the two changes is already lost; 0.4.0 changes which one. Merging such changes is tracked in [#376](https://github.com/allen0099/FastAPI-CacheX/issues/376). +- The backend gains `set_if_equals(key, expected, value, ttl=None)`, next to `delete_if_equals` and `expire_if_equals`. The base class provides a non-atomic fallback, so a custom backend keeps working unchanged; override it to make the save atomic. ### Token sources {#token-source-priority} @@ -226,7 +231,7 @@ CacheManagerProxy.set(CacheManager(lock=True)) - Keys start with a format tag, such as `http:v2|`, so the next format change can remove old keys by pattern. - The host is normalised: lower-cased, and the scheme's default port (`:80`, `:443`) dropped. - A long query string (over about 200 bytes) is stored as `sha256:` and its hex digest; the path stays readable. -- Query parameters are sorted, as `@cache(sort_query=True)` does since 0.3.9; whether that becomes the default or stays opt-in is not decided yet. +- Query parameters are sorted by name: `sort_query` (opt-in since 0.3.9) defaults to `True` in `@cache`, `build_cache_key()` and `invalidate()`, so `?b=2&a=1` and `?a=1&b=2` share one entry. - One `CacheKey` type encodes and parses keys; the key-parsing internals of `routes.py` change. ```text @@ -238,6 +243,7 @@ What to change: - `clear_pattern()` patterns that spell out the separator (`"GET|||*|||/users/*"`) need rewriting. `clear_path()` and `invalidate()` build the key themselves and need nothing. - A custom `key_builder` that calls `build_cache_key()` or joins with `CACHE_KEY_SEPARATOR` follows automatically; one that hard-codes `|||` does not. +- A handler whose response depends on the order of the query string as sent, such as a self or pagination link copied from `request.url` or a signature over the raw query, should set `@cache(sort_query=False)`. Otherwise the first caller's order is cached and served to callers who sent another. `sort_query=False` works on 0.3.9 already. - Entries written by 0.3.x are not read by 0.4.0. They expire on their TTL; on Redis and memory you can remove them right after the upgrade with `await backend.clear_pattern("*|||*")`. Memcached cannot enumerate keys, so there they just expire. 0.3.9 does not warn: nothing in 0.3.x can tell whether a pattern or key builder will match the new format, and the only runtime cost is the one-off miss. diff --git a/i18n/zh-TW/docs/HTTP_CACHING.md b/i18n/zh-TW/docs/HTTP_CACHING.md index b417fb2..3c7d6be 100644 --- a/i18n/zh-TW/docs/HTTP_CACHING.md +++ b/i18n/zh-TW/docs/HTTP_CACHING.md @@ -180,7 +180,7 @@ async def search(q: str, limit: int = 10): return await run_search(q, limit) ``` -此時會先依名稱排序參數,再建立快取鍵。排序是穩定的:同名參數的多個值保留用戶端送出的順序,因為以 `tag: list[str]` 讀取的處理函式看到的正是這個順序,所以 `?tag=b&tag=a` 與 `?tag=a&tag=b` 仍是兩筆項目。名稱以解碼後的值比較(`%61` 視為 `a` 排序,快取鍵本來就這樣寫它),每個名稱與值的編碼都與未排序的快取鍵完全相同,只有順序改變:已經依序排列的查詢,不論是否開啟此選項都得到相同的鍵。快取鍵原本就視為相同的仍然相同(`?a` 與 `?a=`、`&&` 產生的空段),其餘一律不會合併。預設為 `False`,現有的快取鍵都不會改變。 +此時會先依名稱排序參數,再建立快取鍵。排序是穩定的:同名參數的多個值保留用戶端送出的順序,因為以 `tag: list[str]` 讀取的處理函式看到的正是這個順序,所以 `?tag=b&tag=a` 與 `?tag=a&tag=b` 仍是兩筆項目。名稱以解碼後的值比較(`%61` 視為 `a` 排序,快取鍵本來就這樣寫它),每個名稱與值的編碼都與未排序的快取鍵完全相同,只有順序改變:已經依序排列的查詢,不論是否開啟此選項都得到相同的鍵。快取鍵原本就視為相同的仍然相同(`?a` 與 `?a=`、`&&` 產生的空段),其餘一律不會合併。預設為 `False`,現有的快取鍵都不會改變。0.4.0 會隨其他快取鍵變更一起把預設改為 `True`([#72](https://github.com/allen0099/FastAPI-CacheX/issues/72),見[遷移至 0.4.0](MIGRATING_0_4.md#cache-keys))。依賴用戶端送出之查詢順序的處理函式可以維持 `sort_query=False`。 `sort_query` 只套用於預設的 key builder。與自訂的 `key_builder` 一起使用時,套用裝飾器就會拋出 `CacheXError`;請改在 builder 中呼叫 `build_cache_key(request, ..., sort_query=True)`。對這樣的路由呼叫 `invalidate()` 時也要傳入 `sort_query=True`(見[使單一快取路由失效](#invalidating-a-single-cached-route))。 diff --git a/i18n/zh-TW/docs/MIGRATING_0_4.md b/i18n/zh-TW/docs/MIGRATING_0_4.md index abc5476..1711f78 100644 --- a/i18n/zh-TW/docs/MIGRATING_0_4.md +++ b/i18n/zh-TW/docs/MIGRATING_0_4.md @@ -86,16 +86,16 @@ config = SessionConfig( ### 登入與登出 {#login-logout} -0.4.0 讓使用者只能透過一個明確的 API 成為已驗證狀態,而這個 API 一律會發出新的 Session ID([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256))。目前已知: +0.4.0 讓使用者只能透過一個明確的 API 成為已驗證狀態,而這個 API 一律會發出新的 Session ID([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256)): - `login(request, user)`(0.3.9 已提供,`from fastapi_cachex.session import login`)會附加使用者並輪替 ID。現在就請改用它,而不是自行設定 `session.user`。 -- 新增登出 API;`request.session.clear()` 仍代表登出。 -- `Session.user` 在這些呼叫之外變成唯讀。直接指定它的程式碼會失效。 -- 登入會開始一個新的 Session,而不是將匿名 Session 升級。要保留哪些資料(預設全部,或使用 `keep=` 清單)尚未決定。 -- 輪替後的幾秒內,舊 ID 會解析到新的 Session,讓仍帶著舊權杖的進行中請求不會失敗。 -- `rotate_session_id()` 會保留,用於不更換使用者的權限變更,名稱可能會改變。 +- 新增 `await logout(request)`:刪除 Session,Cookie 用戶端會收到讓 Cookie 過期的回應。`request.session.clear()` 仍代表登出。 +- `Session.user` 在 `login()` 與 `SessionManager.create_session(user=...)` 之外變成唯讀。直接指定它的程式碼會失效:使用中介軟體時請改用 `login()`;沒有中介軟體時,請以 `create_session(user=...)` 建立 Session。 +- 登入時預設會帶入匿名 Session 的所有資料,因此購物車在登入後仍會保留。選用的 `keep=` 參數可以縮小範圍(`keep=["cart"]`,或以 `keep=[]` 什麼都不帶)。 +- `login()` 或 `rotate_session_id()` 輪替 ID 後,舊 ID 立即失效,沒有寬限期:若有寬限期,被植入的權杖在這段期間會解析到已登入的 Session。 +- `rotate_session_id()` 保留原名,用於不更換使用者的權限變更。 -確切的函式簽章尚未定案,因此 0.3.9 不會針對這些變更發出警告。若應用程式依據自己寫入 `request.session` 的鍵(`request.session.get("user_id")`)判斷是否已登入,這不在函式庫能察覺的範圍內;請使用函式庫的身分(`session.user`、`AuthenticatedSession`),或自行輪替 ID。 +這些大多是新的 API,而 0.3.x 無從判斷哪些程式碼會指定 `session.user`,因此 0.3.9 不會發出警告。若應用程式依據自己寫入 `request.session` 的鍵(`request.session.get("user_id")`)判斷是否已登入,這不在函式庫能察覺的範圍內;請使用函式庫的身分(`session.user`、`AuthenticatedSession`),或自行輪替 ID。 修改前: @@ -181,7 +181,12 @@ SessionConfig( ### Session 寫入 {#session-writes} -0.4.0 會以有條件的方式寫入 Session([#128](https://github.com/allen0099/FastAPI-CacheX/issues/128)):若某個請求載入 Session 之後,另一個請求刪除、使其失效或輪替了它,前者儲存時不會再讓紀錄復活。不需要修改程式碼。自訂後端必須支援這項變更新增的「存在才寫入」原語,其形式尚未決定。 +0.4.0 會以有條件的方式寫入 Session([#128](https://github.com/allen0099/FastAPI-CacheX/issues/128)):若某個請求載入 Session 之後,另一個請求刪除、使其失效或輪替了它,前者儲存時不會再讓紀錄復活。不需要修改程式碼。 + +- 一般的儲存改為有條件寫入:中介軟體儲存 `request.session` 的修改、滑動續期,以及 `update_session()`。只有在後端的紀錄仍等於這個請求最後一次讀到或寫入的值時才會成功。刪除、使其失效、過期與輪替 ID 仍無條件執行,因此安全動作永遠優先。 +- 被拒絕的儲存會被捨棄並記錄 log。回應照常送出,但不附 Session 權杖。 +- **副作用:**兩個請求同時修改同一個 Session 時(例如兩個分頁同時加入購物車),先儲存的成功,後儲存的被捨棄。目前是後儲存的覆蓋先儲存的,本來就會遺失其中一筆修改;0.4.0 改變的是遺失哪一筆。合併這類修改的做法由 [#376](https://github.com/allen0099/FastAPI-CacheX/issues/376) 追蹤。 +- 後端新增 `set_if_equals(key, expected, value, ttl=None)`,與 `delete_if_equals`、`expire_if_equals` 同一系列。基底類別提供不具原子性的預設實作,因此自訂後端不需修改也能運作;覆寫它才能讓儲存具有原子性。 ### 權杖來源 {#token-source-priority} @@ -225,7 +230,7 @@ CacheManagerProxy.set(CacheManager(lock=True)) - 鍵以格式標籤開頭,例如 `http:v2|`,讓下一次格式變更可以用模式移除舊鍵。 - 主機名稱會正規化:轉為小寫,並去除該 scheme 的預設連接埠(`:80`、`:443`)。 - 過長的查詢字串(約超過 200 位元組)會以 `sha256:` 加上十六進位摘要儲存;路徑仍保持可讀。 -- 查詢參數會排序,如同 0.3.9 起的 `@cache(sort_query=True)`;這會成為預設還是維持選用,尚未決定。 +- 查詢參數會依名稱排序:`sort_query`(0.3.9 起可選用)在 `@cache`、`build_cache_key()` 與 `invalidate()` 中預設為 `True`,因此 `?b=2&a=1` 與 `?a=1&b=2` 共用同一筆項目。 - 由單一的 `CacheKey` 型別負責編碼與解析鍵;`routes.py` 中解析鍵的內部實作會改變。 ```text @@ -237,6 +242,7 @@ CacheManagerProxy.set(CacheManager(lock=True)) - 直接寫出分隔符號的 `clear_pattern()` 模式(`"GET|||*|||/users/*"`)需要改寫。`clear_path()` 與 `invalidate()` 會自行組出鍵,不需要修改。 - 呼叫 `build_cache_key()` 或以 `CACHE_KEY_SEPARATOR` 串接的自訂 `key_builder` 會自動跟上;直接寫死 `|||` 的則不會。 +- 回應取決於用戶端送出的查詢字串順序的處理函式(例如從 `request.url` 複製的自身連結或分頁連結、對原始查詢字串計算的簽章),請設定 `@cache(sort_query=False)`。否則第一位呼叫者的順序會被快取,並提供給送出其他順序的呼叫者。`sort_query=False` 在 0.3.9 就能使用。 - 0.4.0 不會讀取 0.3.x 寫入的項目。這些項目會在 TTL 到期後過期;在 Redis 與記憶體後端上,可以在升級後立即以 `await backend.clear_pattern("*|||*")` 移除。Memcached 無法列舉鍵,只能等它們過期。 0.3.9 不會警告:0.3.x 無從判斷某個模式或 key builder 是否符合新格式,而執行期唯一的代價只是一次未命中。