Skip to content
12 changes: 10 additions & 2 deletions fast_cache_middleware/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from starlette.requests import Request
from starlette.responses import Response

from .exceptions import NotFoundStorageError, TTLExpiredStorageError
from .schemas import CacheConfiguration
from .storages import BaseStorage

Expand Down Expand Up @@ -181,7 +182,7 @@ async def cache_response(

async def get_cached_response(
self, cache_key: str, storage: BaseStorage
) -> Optional[Response]:
) -> Response | None:
"""Gets cached response if it exists and is valid.

Args:
Expand All @@ -191,9 +192,16 @@ async def get_cached_response(
Returns:
Response or None if cache is invalid/missing
"""
result = await storage.get(cache_key)

try:
result = await storage.get(cache_key)
except (NotFoundStorageError, TTLExpiredStorageError) as e:
logger.warning(e)
return None

if result is None:
return None

response, _, _ = result
return response

Expand Down
10 changes: 10 additions & 0 deletions fast_cache_middleware/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ class FastCacheMiddlewareError(Exception):

class StorageError(FastCacheMiddlewareError):
pass


class NotFoundStorageError(StorageError):
def __init__(self, key: str, message: str = "Data not found") -> None:
super().__init__(f"{message}. Key: {key}.")


class TTLExpiredStorageError(StorageError):
def __init__(self, key: str, message: str = "TTL expired") -> None:
super().__init__(f"{message}. Key: {key}.")
11 changes: 7 additions & 4 deletions fast_cache_middleware/storages/in_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from starlette.requests import Request
from starlette.responses import Response

from fast_cache_middleware.exceptions import StorageError
from fast_cache_middleware.exceptions import (
NotFoundStorageError,
StorageError,
TTLExpiredStorageError,
)
from fast_cache_middleware.serializers import BaseSerializer, Metadata

from .base_storage import BaseStorage, StoredResponse
Expand Down Expand Up @@ -101,13 +105,12 @@ async def get(self, key: str) -> Optional[StoredResponse]:
Tuple (response, request, metadata) if found and not expired, None if not found or expired
"""
if key not in self._storage:
return None
raise NotFoundStorageError(key)

# Lazy TTL check
if self._is_expired(key):
self._pop_item(key)
logger.debug("Element %s removed from cache - TTL expired", key)
return None
raise TTLExpiredStorageError(key)

self._storage.move_to_end(key)

Expand Down
24 changes: 13 additions & 11 deletions fast_cache_middleware/storages/redis_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from starlette.requests import Request
from starlette.responses import Response

from fast_cache_middleware.exceptions import StorageError
from fast_cache_middleware.exceptions import (
NotFoundStorageError,
StorageError,
TTLExpiredStorageError,
)
from fast_cache_middleware.serializers import BaseSerializer, JSONSerializer, Metadata

from .base_storage import BaseStorage, StoredResponse
Expand Down Expand Up @@ -62,25 +66,23 @@ async def set(
await self._storage.set(full_key, value, ex=ttl)
logger.info("Data written to Redis")

async def get(self, key: str) -> Optional[StoredResponse]:
async def get(self, key: str) -> StoredResponse:
"""
Get response from Redis. If TTL expired returns None.
"""
full_key = self._full_key(key)

if not await self._storage.exists(full_key):
raise TTLExpiredStorageError(full_key)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сюда добавил проверку на существование ключа, аналогично тому, что сделано в in_memory решении. Если это избыточно и хватит простой проверки на наличие данных, которая ниже ( 80 строчка ), то уберу.


raw_data = await self._storage.get(full_key)

if raw_data is None:
logger.debug("Key %s will be removed from Redis - TTL expired", full_key)
return None
raise NotFoundStorageError(key)

logger.debug(f"Takin data from Redis: %s", raw_data)
try:
return self._serializer.loads(raw_data)
except Exception as e:
logger.warning(
"Failed to deserialize cached response for key %s: %s", key, e
)
return None

return self._serializer.loads(raw_data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Redis Storage Fails on Deserialization Errors

The RedisStorage.get method no longer handles deserialization errors. Previously, exceptions from self._serializer.loads(raw_data) (e.g., ValueError, JSONDecodeError) were caught, logged as warnings, and None was returned. Now, these exceptions propagate uncaught, potentially crashing the application when corrupted data is encountered in the cache, as the calling code expects NotFoundError for missing keys but not other serialization failures.

Locations (1)

Fix in CursorFix in Web


async def delete(self, path: re.Pattern) -> None:
"""
Expand Down
Loading