diff --git a/fastapi/routing.py b/fastapi/routing.py index 36acb6b89d..0ce1e1904b 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -28,6 +28,7 @@ from typing import ( TypeVar, cast, ) +from weakref import WeakKeyDictionary import anyio from annotated_doc import Doc @@ -248,15 +249,17 @@ class _DefaultLifespan: # Cache for endpoint context to avoid re-extracting on every request -_endpoint_context_cache: dict[int, EndpointContext] = {} +_endpoint_context_cache: WeakKeyDictionary[Any, EndpointContext] = WeakKeyDictionary() def _extract_endpoint_context(func: Any) -> EndpointContext: """Extract endpoint context with caching to avoid repeated file I/O.""" - func_id = id(func) - - if func_id in _endpoint_context_cache: - return _endpoint_context_cache[func_id] + try: + cached = _endpoint_context_cache.get(func) + except TypeError: + cached = None + if cached is not None: + return cached try: ctx: EndpointContext = {} @@ -270,7 +273,10 @@ def _extract_endpoint_context(func: Any) -> EndpointContext: except Exception: ctx = EndpointContext() - _endpoint_context_cache[func_id] = ctx + try: + _endpoint_context_cache[func] = ctx + except TypeError: + pass return ctx diff --git a/tests/test_routing_endpoint_context_cache.py b/tests/test_routing_endpoint_context_cache.py new file mode 100644 index 0000000000..30435fda8e --- /dev/null +++ b/tests/test_routing_endpoint_context_cache.py @@ -0,0 +1,25 @@ +import gc +import weakref + +from fastapi.routing import _endpoint_context_cache, _extract_endpoint_context + + +def _make_endpoint(): + def endpoint(): + return None + + return endpoint + + +def test_endpoint_context_cache_releases_endpoints(): + endpoint = _make_endpoint() + _extract_endpoint_context(endpoint) + assert endpoint in _endpoint_context_cache + + ref = weakref.ref(endpoint) + size_with_endpoint = len(_endpoint_context_cache) + del endpoint + gc.collect() + + assert ref() is None + assert len(_endpoint_context_cache) <= size_with_endpoint - 1