diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ca43c..4a4c8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,11 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2. accepted too, and a header without a token no longer yields an empty one. ([#166](https://github.com/allen0099/FastAPI-CacheX/issues/166)) +- **Memcached `clear_path()` no longer reports a connection failure as + "nothing to clear".** It caught every exception and returned `0`, while + `delete()` and the other methods let the error through. + ([#177](https://github.com/allen0099/FastAPI-CacheX/issues/177)) + ## [0.3.7] - 2026-09-25 ### Added diff --git a/fastapi_cachex/backends/memcached.py b/fastapi_cachex/backends/memcached.py index 1355e01..89476b5 100644 --- a/fastapi_cachex/backends/memcached.py +++ b/fastapi_cachex/backends/memcached.py @@ -331,20 +331,16 @@ async def clear_path(self, path: str, include_params: bool = False) -> int: # Try to delete the prefixed key (exact match only) prefixed_key = self._make_key(path) - try: - result = await asyncio.to_thread( - self.client.delete, prefixed_key, noreply=False - ) - except Exception: # noqa: BLE001 - return 0 - else: - logger.debug( - "Memcached CLEAR_PATH; path=%s include_params=%s removed=%s", - path, - include_params, - 1 if result else 0, - ) - return 1 if result else 0 + result = await asyncio.to_thread( + self.client.delete, prefixed_key, noreply=False + ) + logger.debug( + "Memcached CLEAR_PATH; path=%s include_params=%s removed=%s", + path, + include_params, + 1 if result else 0, + ) + return 1 if result else 0 async def clear_pattern(self, pattern: str) -> int: """Clear cached responses matching a pattern. diff --git a/tests/backends/test_memcached.py b/tests/backends/test_memcached.py index 4008223..2bcbbfc 100644 --- a/tests/backends/test_memcached.py +++ b/tests/backends/test_memcached.py @@ -1,4 +1,5 @@ import asyncio +import socket import sys from unittest.mock import MagicMock @@ -247,21 +248,35 @@ async def test_memcached_get_invalid_and_missing_fields( assert res2 is None +@pytest.mark.asyncio +async def test_memcached_clear_path_raises_when_the_server_is_unreachable() -> None: + """A connection failure is not "nothing to clear" (#177). + + `clear_path()` used to return 0 for any exception, while `delete()` and + every other method let it through. + """ + with socket.socket() as probe: + probe.bind(("127.0.0.1", 0)) + closed_port = probe.getsockname()[1] + backend = MemcachedBackend(servers=[f"127.0.0.1:{closed_port}"]) + + with pytest.raises(ConnectionRefusedError): + await backend.clear_path("/nope") + + @requires_memcached @pytest.mark.asyncio -async def test_memcached_clear_path_exception( +async def test_memcached_clear_path_propagates_client_errors( monkeypatch, memcached_backend: MemcachedBackend, ) -> None: - """Simulate client.delete raising to hit exception branch in clear_path().""" - def boom(*args, **kwargs) -> None: msg = "delete failed" raise RuntimeError(msg) monkeypatch.setattr(memcached_backend.client, "delete", boom) - cleared = await memcached_backend.clear_path("/nope", include_params=False) - assert cleared == 0 + with pytest.raises(RuntimeError, match="delete failed"): + await memcached_backend.clear_path("/nope", include_params=False) @requires_memcached