From b624a31a0130e3d528862f08e8235068a53b2999 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 382222606..7666e4e56 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -271,6 +271,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 043f68185..d16b12317 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -12,6 +12,7 @@ from fastapi import status +from ..pools import fastapi_app_pool from ..schemas import DemoExceptionType @@ -236,6 +237,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)