Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 10 additions & 14 deletions fastapi_cachex/backends/memcached.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 20 additions & 5 deletions tests/backends/test_memcached.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import socket
import sys
from unittest.mock import MagicMock

Expand Down Expand Up @@ -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
Expand Down
Loading