Skip to content

Commit 4762b2c

Browse files
awanawonaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent b2143ef commit 4762b2c

1 file changed

Lines changed: 13 additions & 2 deletions

File tree

fastapi_cache/decorator.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import logging
23
import sys
34
from functools import wraps
@@ -66,6 +67,16 @@ def _locate_param(
6667
return param
6768

6869

70+
def _compute_etag(data: bytes) -> str:
71+
"""Compute a deterministic ETag hash from cache data.
72+
73+
Uses MD5 for speed since this is for caching, not security.
74+
Unlike Python's built-in hash(), this produces consistent values
75+
across different processes and service restarts.
76+
"""
77+
return hashlib.md5(data).hexdigest()
78+
79+
6980
def _uncacheable(request: Optional[Request]) -> bool:
7081
"""Determine if this request should not be cached
7182
@@ -199,14 +210,14 @@ async def ensure_async_func(*args: P.args, **kwargs: P.kwargs) -> R:
199210
response.headers.update(
200211
{
201212
"Cache-Control": f"max-age={expire}",
202-
"ETag": f"W/{hash(to_cache)}",
213+
"ETag": f"W/{_compute_etag(to_cache)}",
203214
cache_status_header: "MISS",
204215
}
205216
)
206217

207218
else: # cache hit
208219
if response:
209-
etag = f"W/{hash(cached)}"
220+
etag = f"W/{_compute_etag(cached)}"
210221
response.headers.update(
211222
{
212223
"Cache-Control": f"max-age={ttl}",

0 commit comments

Comments
 (0)