Skip to content
13 changes: 9 additions & 4 deletions fast_cache_middleware/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from starlette.responses import Response
from starlette.routing import is_async_callable

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

Expand Down Expand Up @@ -171,7 +171,12 @@ async def cache_response(
"""
if await self.is_cachable_response(response):
response.headers["X-Cache-Status"] = "HIT"
await storage.set(cache_key, response, request, {"ttl": ttl})

try:
await storage.set(cache_key, response, request, {"ttl": ttl})
except FastCacheMiddlewareError as e:
logger.error("Failed to cache response: %s", e)

else:
logger.debug("Skip caching for response: %s", response.status_code)

Expand All @@ -190,8 +195,8 @@ async def get_cached_response(

try:
result = await storage.get(cache_key)
except (NotFoundStorageError, TTLExpiredStorageError) as e:
logger.warning(e)
except FastCacheMiddlewareError as e:
logger.error("Couldn't get the cache: %s", e)
return None

if result is None:
Expand Down
5 changes: 4 additions & 1 deletion fast_cache_middleware/storages/in_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ async def set(
logger.info("Element %s removed from cache - overwrite", key)
self._pop_item(key)

self._storage[key] = (response, request, metadata)
try:
self._storage[key] = (response, request, metadata)
except TypeError as e:
raise StorageError(e)

data_ttl = metadata.get("ttl", self._ttl)
if data_ttl is not None:
Expand Down
11 changes: 9 additions & 2 deletions fast_cache_middleware/storages/redis_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
except ImportError:
redis = None # type: ignore

from redis.exceptions import ConnectionError, TimeoutError
from starlette.requests import Request
from starlette.responses import Response

Expand Down Expand Up @@ -63,7 +64,7 @@ async def set(

full_key = self._full_key(key)

if await self._storage.exists(full_key):
if await self.exists(full_key):
logger.info("Element %s removed from cache - overwrite", key)
await self._storage.delete(full_key)

Expand All @@ -76,7 +77,7 @@ async def get(self, key: str) -> StoredResponse:
"""
full_key = self._full_key(key)

if not await self._storage.exists(full_key):
if not await self.exists(full_key):
raise TTLExpiredStorageError(full_key)

raw_data = await self._storage.get(full_key)
Expand Down Expand Up @@ -108,6 +109,12 @@ async def delete(self, path: re.Pattern) -> None:
await self._storage.delete(value)
logger.info(f"Key deleted from Redis: %s", value)

async def exists(self, key: str) -> int:
try:
return await self._storage.exists(key)
except (TimeoutError, ConnectionError) as e:
raise StorageError(f"Redis error: {e}")

async def close(self) -> None:
await self._storage.flushdb()
logger.debug("Cache storage cleared")
Expand Down
49 changes: 49 additions & 0 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from fast_cache_middleware.controller import Controller
from fast_cache_middleware.exceptions import (
FastCacheMiddlewareError,
NotFoundStorageError,
TTLExpiredStorageError,
)
Expand Down Expand Up @@ -248,3 +249,51 @@ async def test_cache_response_success(
assert call_args[0][1] == mock_response # response
assert call_args[0][2] == mock_request # request
assert call_args[0][3]["ttl"] == 600 # metadata

@pytest.mark.asyncio
async def test_cache_response_raises_and_logs_error(
self,
controller: Controller,
mock_storage: MagicMock,
mock_request: Request,
mock_response: Response,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Проверяет, что при ошибке кэширования пишется лог и исключение не пробрасывается."""

error = FastCacheMiddlewareError("storage failure")
mock_storage.set.side_effect = error

caplog.set_level(logging.WARNING)

await controller.cache_response(
"test_key", mock_request, mock_response, mock_storage, 600
)

mock_storage.set.assert_called_once()

assert "Failed to cache response" in caplog.text
assert "storage failure" in caplog.text

@pytest.mark.asyncio
async def test_get_cache_response_raises_and_logs_error(
self,
controller: Controller,
mock_storage: MagicMock,
mock_request: Request,
mock_response: Response,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Проверяет, что при ошибке получения кэша пишется лог и исключение не пробрасывается."""

error = FastCacheMiddlewareError("storage failure")
mock_storage.get.side_effect = error

caplog.set_level(logging.WARNING)

await controller.get_cached_response("test_key", mock_storage)

mock_storage.get.assert_called_once()

assert "Couldn't get the cache" in caplog.text
assert "storage failure" in caplog.text