diff --git a/fastapi/routing.py b/fastapi/routing.py index 9ca2f46732..4e739725e8 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -19,6 +19,7 @@ from typing import ( Optional, Union, ) +from weakref import WeakKeyDictionary from annotated_doc import Doc from fastapi import params @@ -161,15 +162,17 @@ def _merge_lifespan_context( # 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 = {} @@ -183,7 +186,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