From 431badb2a5bf1c1164bf907d3b54f8d2af6c18a0 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 31 Oct 2025 20:27:46 +0300 Subject: [PATCH 1/9] add try/except to the in-memory storage --- fast_cache_middleware/storages/in_memory_storage.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fast_cache_middleware/storages/in_memory_storage.py b/fast_cache_middleware/storages/in_memory_storage.py index 23c7a7b..de29eb0 100644 --- a/fast_cache_middleware/storages/in_memory_storage.py +++ b/fast_cache_middleware/storages/in_memory_storage.py @@ -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 Exception as e: + raise StorageError(e) data_ttl = metadata.get("ttl", self._ttl) if data_ttl is not None: @@ -114,7 +117,12 @@ async def get(self, key: str) -> Optional[StoredResponse]: self._storage.move_to_end(key) - return self._storage[key] + try: + cache = self._storage[key] + except Exception as e: + raise StorageError(e) + + return cache async def delete(self, path: re.Pattern) -> None: """Removes responses from cache by request path pattern. From 2546a3d27dfda32f45460c9206a9c67cc5bd9804 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 31 Oct 2025 20:28:28 +0300 Subject: [PATCH 2/9] rise StorageError if RedisError --- fast_cache_middleware/storages/redis_storage.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fast_cache_middleware/storages/redis_storage.py b/fast_cache_middleware/storages/redis_storage.py index d03b869..63e6482 100644 --- a/fast_cache_middleware/storages/redis_storage.py +++ b/fast_cache_middleware/storages/redis_storage.py @@ -8,6 +8,7 @@ except ImportError: redis = None # type: ignore +from redis.exceptions import RedisError from starlette.requests import Request from starlette.responses import Response @@ -63,7 +64,7 @@ async def set( full_key = self._full_key(key) - if await self._storage.exists(full_key): + if await self._check_exists(full_key): logger.info("Element %s removed from cache - overwrite", key) await self._storage.delete(full_key) @@ -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._check_exists(full_key): raise TTLExpiredStorageError(full_key) raw_data = await self._storage.get(full_key) @@ -114,3 +115,9 @@ async def close(self) -> None: def _full_key(self, key: str) -> str: return f"{self._namespace}:{key}" + + async def _check_exists(self, key: str) -> int: + try: + return await self._storage.exists(key) + except RedisError as e: + raise StorageError(e) From ff6f13133ca025bc7b7e9c3c9fe229e48c872423 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 31 Oct 2025 20:29:14 +0300 Subject: [PATCH 3/9] output an error to the log when caching or receiving the cache --- fast_cache_middleware/controller.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fast_cache_middleware/controller.py b/fast_cache_middleware/controller.py index 27a23f6..7944fc8 100644 --- a/fast_cache_middleware/controller.py +++ b/fast_cache_middleware/controller.py @@ -9,7 +9,9 @@ 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 @@ -171,7 +173,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.warning("Failed to cache response: %s", e) + else: logger.debug("Skip caching for response: %s", response.status_code) @@ -190,8 +197,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.warning("Couldn't get the cache: %s", e) return None if result is None: From 026bf154f102ed120fefadf090b6ad2dcc86a86d Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 31 Oct 2025 20:29:43 +0300 Subject: [PATCH 4/9] add tests to check the log output --- fast_cache_middleware/controller.py | 4 +-- tests/test_controller.py | 49 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/fast_cache_middleware/controller.py b/fast_cache_middleware/controller.py index 7944fc8..fae68d0 100644 --- a/fast_cache_middleware/controller.py +++ b/fast_cache_middleware/controller.py @@ -9,9 +9,7 @@ from starlette.responses import Response from starlette.routing import is_async_callable -from .exceptions import ( - FastCacheMiddlewareError, -) +from .exceptions import FastCacheMiddlewareError from .schemas import CacheConfiguration from .storages import BaseStorage diff --git a/tests/test_controller.py b/tests/test_controller.py index aabbaa2..f56bf04 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -10,6 +10,7 @@ from fast_cache_middleware.controller import Controller from fast_cache_middleware.exceptions import ( + FastCacheMiddlewareError, NotFoundStorageError, TTLExpiredStorageError, ) @@ -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 From 6acba7b92339b6fa5089b8a9819ed44846e813ed Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 11 Nov 2025 10:51:36 +0300 Subject: [PATCH 5/9] change warning to error --- fast_cache_middleware/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fast_cache_middleware/controller.py b/fast_cache_middleware/controller.py index fae68d0..068a7af 100644 --- a/fast_cache_middleware/controller.py +++ b/fast_cache_middleware/controller.py @@ -175,7 +175,7 @@ async def cache_response( try: await storage.set(cache_key, response, request, {"ttl": ttl}) except FastCacheMiddlewareError as e: - logger.warning("Failed to cache response: %s", e) + logger.error("Failed to cache response: %s", e) else: logger.debug("Skip caching for response: %s", response.status_code) @@ -196,7 +196,7 @@ async def get_cached_response( try: result = await storage.get(cache_key) except FastCacheMiddlewareError as e: - logger.warning("Couldn't get the cache: %s", e) + logger.error("Couldn't get the cache: %s", e) return None if result is None: From 0179a8e648fc8da8b2e79bebe6c87046d466f16c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 11 Nov 2025 11:10:42 +0300 Subject: [PATCH 6/9] change Exception to TypeError --- fast_cache_middleware/storages/in_memory_storage.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/fast_cache_middleware/storages/in_memory_storage.py b/fast_cache_middleware/storages/in_memory_storage.py index de29eb0..3d4dfa2 100644 --- a/fast_cache_middleware/storages/in_memory_storage.py +++ b/fast_cache_middleware/storages/in_memory_storage.py @@ -84,7 +84,7 @@ async def set( try: self._storage[key] = (response, request, metadata) - except Exception as e: + except TypeError as e: raise StorageError(e) data_ttl = metadata.get("ttl", self._ttl) @@ -117,12 +117,7 @@ async def get(self, key: str) -> Optional[StoredResponse]: self._storage.move_to_end(key) - try: - cache = self._storage[key] - except Exception as e: - raise StorageError(e) - - return cache + return self._storage[key] async def delete(self, path: re.Pattern) -> None: """Removes responses from cache by request path pattern. From 0241eb43c8a30355a4c994070c81fada94bf1f60 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 11 Nov 2025 12:07:41 +0300 Subject: [PATCH 7/9] refactor RedisStorage to handle only expected errors --- fast_cache_middleware/storages/redis_storage.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/fast_cache_middleware/storages/redis_storage.py b/fast_cache_middleware/storages/redis_storage.py index 63e6482..d4d350a 100644 --- a/fast_cache_middleware/storages/redis_storage.py +++ b/fast_cache_middleware/storages/redis_storage.py @@ -8,7 +8,7 @@ except ImportError: redis = None # type: ignore -from redis.exceptions import RedisError +from redis.exceptions import ConnectionError, TimeoutError from starlette.requests import Request from starlette.responses import Response @@ -109,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") @@ -117,7 +123,4 @@ def _full_key(self, key: str) -> str: return f"{self._namespace}:{key}" async def _check_exists(self, key: str) -> int: - try: - return await self._storage.exists(key) - except RedisError as e: - raise StorageError(e) + return await self.exists(key) From c7da066c01062c08b7d167e41116e267249c51e7 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 18 Nov 2025 16:48:37 +0300 Subject: [PATCH 8/9] removing an unusual method --- fast_cache_middleware/storages/redis_storage.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fast_cache_middleware/storages/redis_storage.py b/fast_cache_middleware/storages/redis_storage.py index d4d350a..4286ae4 100644 --- a/fast_cache_middleware/storages/redis_storage.py +++ b/fast_cache_middleware/storages/redis_storage.py @@ -64,7 +64,7 @@ async def set( full_key = self._full_key(key) - if await self._check_exists(full_key): + if await self.exists(full_key): logger.info("Element %s removed from cache - overwrite", key) await self._storage.delete(full_key) @@ -77,7 +77,7 @@ async def get(self, key: str) -> StoredResponse: """ full_key = self._full_key(key) - if not await self._check_exists(full_key): + if not await self.exists(full_key): raise TTLExpiredStorageError(full_key) raw_data = await self._storage.get(full_key) @@ -121,6 +121,3 @@ async def close(self) -> None: def _full_key(self, key: str) -> str: return f"{self._namespace}:{key}" - - async def _check_exists(self, key: str) -> int: - return await self.exists(key) From 19c113a97d2900b2c991632eb492998f02b9442f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 18 Nov 2025 16:48:37 +0300 Subject: [PATCH 9/9] removing an unnecessary method --- fast_cache_middleware/storages/redis_storage.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fast_cache_middleware/storages/redis_storage.py b/fast_cache_middleware/storages/redis_storage.py index d4d350a..4286ae4 100644 --- a/fast_cache_middleware/storages/redis_storage.py +++ b/fast_cache_middleware/storages/redis_storage.py @@ -64,7 +64,7 @@ async def set( full_key = self._full_key(key) - if await self._check_exists(full_key): + if await self.exists(full_key): logger.info("Element %s removed from cache - overwrite", key) await self._storage.delete(full_key) @@ -77,7 +77,7 @@ async def get(self, key: str) -> StoredResponse: """ full_key = self._full_key(key) - if not await self._check_exists(full_key): + if not await self.exists(full_key): raise TTLExpiredStorageError(full_key) raw_data = await self._storage.get(full_key) @@ -121,6 +121,3 @@ async def close(self) -> None: def _full_key(self, key: str) -> str: return f"{self._namespace}:{key}" - - async def _check_exists(self, key: str) -> int: - return await self.exists(key)