Avatars: a cached avatar or miss failed on every later request
The shared Redis client decodes replies as UTF-8 text, but the avatar cache holds raw image bytes and a b"none" miss marker: an image could not be read back at all, and a miss came back as str and crashed on .partition(b"\n"). Only a sender's first request worked; after that, a 500. The cache now goes through a binary-safe client, get_redis_bytes(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ from fastapi import APIRouter, Query, Response
|
||||
from app.accounts.ssrf import SsrfError, guarded_request
|
||||
from app.config import get_settings
|
||||
from app.identity.deps import CurrentUser
|
||||
from app.redis_client import get_redis
|
||||
from app.redis_client import get_redis_bytes
|
||||
|
||||
router = APIRouter(prefix="/avatar", tags=["mail"])
|
||||
|
||||
@@ -66,7 +66,8 @@ async def sender_avatar(_user: CurrentUser, email: str = Query(...)) -> Response
|
||||
if not get_settings().sender_avatars_enabled or "@" not in email:
|
||||
return Response(status_code=404)
|
||||
|
||||
redis = get_redis()
|
||||
# Binary-safe client: the cache holds raw image bytes (and a b"none" marker).
|
||||
redis = get_redis_bytes()
|
||||
md5 = _email_md5(email)
|
||||
key = f"avatar:{md5}"
|
||||
headers = {"Cache-Control": "private, max-age=86400"}
|
||||
|
||||
@@ -20,6 +20,7 @@ def aw(result: Any) -> Awaitable[Any]:
|
||||
|
||||
|
||||
_client: aioredis.Redis | None = None
|
||||
_bytes_client: aioredis.Redis | None = None
|
||||
|
||||
|
||||
def get_redis() -> aioredis.Redis:
|
||||
@@ -34,9 +35,25 @@ def get_redis() -> aioredis.Redis:
|
||||
return _client
|
||||
|
||||
|
||||
def get_redis_bytes() -> aioredis.Redis:
|
||||
"""Return the process-wide Redis client for binary values.
|
||||
|
||||
``get_redis()`` decodes every reply as UTF-8 text, which fails on (or
|
||||
silently changes the type of) raw bytes such as a cached image. Anything that
|
||||
stores bytes goes through this one instead.
|
||||
"""
|
||||
global _bytes_client
|
||||
if _bytes_client is None:
|
||||
_bytes_client = aioredis.Redis.from_url(get_settings().redis_url, decode_responses=False)
|
||||
return _bytes_client
|
||||
|
||||
|
||||
async def close_redis() -> None:
|
||||
"""Close the Redis client (used on app shutdown)."""
|
||||
global _client
|
||||
"""Close the Redis clients (used on app shutdown)."""
|
||||
global _client, _bytes_client
|
||||
if _client is not None:
|
||||
await _client.aclose()
|
||||
_client = None
|
||||
if _bytes_client is not None:
|
||||
await _bytes_client.aclose()
|
||||
_bytes_client = None
|
||||
|
||||
Reference in New Issue
Block a user