diff --git a/fastapi/routing.py b/fastapi/routing.py index 78207778a..996a0040d 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -9,6 +9,7 @@ import os import stat import threading import types +import weakref from collections.abc import ( AsyncIterator, Awaitable, @@ -264,16 +265,19 @@ class _DefaultLifespan: return self -# Cache for endpoint context to avoid re-extracting on every request -_endpoint_context_cache: dict[int, EndpointContext] = {} +# Cache for endpoint context to avoid re-extracting on every request. +# Uses WeakKeyDictionary so entries are automatically evicted when the endpoint +# function is garbage collected (e.g., when a dynamically created FastAPI app is +# destroyed), preventing both memory leaks and stale-ID lookups. +_endpoint_context_cache: weakref.WeakKeyDictionary[ + Callable[..., Any], EndpointContext +] = weakref.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] + if func in _endpoint_context_cache: + return _endpoint_context_cache[func] try: ctx: EndpointContext = {} @@ -287,7 +291,7 @@ def _extract_endpoint_context(func: Any) -> EndpointContext: except Exception: ctx = EndpointContext() - _endpoint_context_cache[func_id] = ctx + _endpoint_context_cache[func] = ctx return ctx