From 4762b2c59f6948e9ada6f9ed544ffee39ea3c5dc Mon Sep 17 00:00:00 2001 From: awanawona Date: Mon, 23 Mar 2026 15:37:24 +0800 Subject: [PATCH] fix: use deterministic hash for ETag generation Python's built-in hash() is salted by default (PYTHONHASHSEED), producing different values across processes. This causes ETags to change on service restart or differ between backends behind a load balancer, invalidating the cache unexpectedly. Replace hash() with hashlib.md5() which produces consistent values. MD5 is used for speed since this is for caching, not security. Fixes #400 Co-Authored-By: Claude Opus 4.5 --- fastapi_cache/decorator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fastapi_cache/decorator.py b/fastapi_cache/decorator.py index 7df09e88..510ee0e1 100644 --- a/fastapi_cache/decorator.py +++ b/fastapi_cache/decorator.py @@ -1,3 +1,4 @@ +import hashlib import logging import sys from functools import wraps @@ -66,6 +67,16 @@ def _locate_param( return param +def _compute_etag(data: bytes) -> str: + """Compute a deterministic ETag hash from cache data. + + Uses MD5 for speed since this is for caching, not security. + Unlike Python's built-in hash(), this produces consistent values + across different processes and service restarts. + """ + return hashlib.md5(data).hexdigest() + + def _uncacheable(request: Optional[Request]) -> bool: """Determine if this request should not be cached @@ -199,14 +210,14 @@ async def ensure_async_func(*args: P.args, **kwargs: P.kwargs) -> R: response.headers.update( { "Cache-Control": f"max-age={expire}", - "ETag": f"W/{hash(to_cache)}", + "ETag": f"W/{_compute_etag(to_cache)}", cache_status_header: "MISS", } ) else: # cache hit if response: - etag = f"W/{hash(cached)}" + etag = f"W/{_compute_etag(cached)}" response.headers.update( { "Cache-Control": f"max-age={ttl}",