From 18942e8a8bc32c403fa159a0b1b61046133b6aad Mon Sep 17 00:00:00 2001 From: SparkDrago05 Date: Thu, 22 Oct 2026 17:57:06 +0500 Subject: [PATCH] [FIX] fastapi: pool apps by endpoint root path, not request path FastApiDispatcher passed the full request path as the key of the app pool. Every distinct path under an endpoint, including unknown paths and path parameters, built and kept its own FastAPI application until the registry cache was invalidated, so worker memory grew with the number of distinct paths requested, even unauthenticated ones. Resolve the endpoint's root path once per request and use it as the pool key and for the uid lookup. Fixes #656 Assisted-by: Claude Opus 5.5 Signed-off-by: SparkDrago05 --- fastapi/fastapi_dispatcher.py | 7 +++++-- fastapi/models/fastapi_endpoint.py | 11 +++++++++++ fastapi/tests/test_fastapi.py | 12 ++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index 79b80b753..27faee852 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -28,10 +28,13 @@ def dispatch(self, endpoint, args): self.request.params = {} # dict(self.request.get_http_params(), **args) environ = self._get_environ() path = environ["PATH_INFO"] + endpoint_model = request.env["fastapi.endpoint"].sudo() + # Ensure that only one app exists by endpoint's root path + root_path = endpoint_model._get_root_path(path) or path # TODO store the env into contextvar to be used by the odoo_env # depends method - with fastapi_app_pool.get_app(env=request.env, root_path=path) as app: - uid = request.env["fastapi.endpoint"].sudo().get_uid(path) + with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app: + uid = endpoint_model.get_uid(root_path) data = BytesIO() with self._manage_odoo_env(uid): for r in app(environ, self._make_response): diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 388e56a6d..ee3c94424 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -266,6 +266,17 @@ def _get_id_for_path(self, path): root_path = self._find_first_matching_url_path(id_by_path.keys(), path) return id_by_path.get(root_path) + @api.model + def _get_root_path(self, path): + """Return the root path of the endpoint that serves the given path. + + Not cached on the path: a request path can contain anything, so caching + on it would keep one entry per distinct path. + """ + return self._find_first_matching_url_path( + self._get_id_by_root_path_map().keys(), path + ) + @api.model def _get_endpoint(self, path): id_ = self._get_id_for_path(path) diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index e074325e0..7525372ec 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -11,6 +11,7 @@ from fastapi import status +from ..pools import fastapi_app_pool from ..schemas import DemoExceptionType @@ -195,6 +196,17 @@ def test_url_matching(self): "/fastapi/v1", ) + def test_app_pool_keyed_on_root_path(self): + # Calls to endpoints under the same root path are processed by the same + # app: only one app is initialized by root path + for route in ("/fastapi_demo/demo", "/fastapi_demo/demo/lang"): + response = self.url_open(route, timeout=20) + self.assertEqual(response.status_code, 200) + pools = fastapi_app_pool._queue_by_db_by_root_path[self.env.cr.dbname] + self.assertIn(self.fastapi_demo_app.root_path, pools) + self.assertNotIn("/fastapi_demo/demo", pools) + self.assertNotIn("/fastapi_demo/demo/lang", pools) + def test_multi_slash(self): route = "/fastapi/demo-multi/demo/" response = self.url_open(route, timeout=20)