diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index c983c9a1c..150af98c8 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -22,10 +22,10 @@ "demo": ["demo/fastapi_endpoint_demo.xml"], "external_dependencies": { "python": [ - "fastapi", + "fastapi>=0.110.0", "python-multipart", "ujson", - "a2wsgi", + "a2wsgi>=1.10.6", "parse-accept-language", ] }, diff --git a/fastapi/dependencies.py b/fastapi/dependencies.py index 4835c74c9..47b133285 100644 --- a/fastapi/dependencies.py +++ b/fastapi/dependencies.py @@ -101,7 +101,7 @@ def optionally_authenticated_partner( def paging( - page: Annotated[int, Query(gte=1)] = 1, page_size: Annotated[int, Query(gte=1)] = 80 + page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1)] = 80 ) -> Paging: """Return a Paging object from the page and page_size parameters""" return Paging(limit=page_size, offset=(page - 1) * page_size) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index dda4b20e4..8bd9aa412 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -112,7 +112,21 @@ def _get_environ(self): # date odoo version. (EAFP: Easier to Ask for Forgiveness than Permission) httprequest = self.request.httprequest environ = httprequest.environ - environ["wsgi.input"] = httprequest._get_stream_for_parsing() + stream = httprequest._get_stream_for_parsing() + # Check if the stream supports seeking + if hasattr(stream, "seekable") and stream.seekable(): + # Reset the stream to the beginning to ensure it can be consumed + # again by the application in case of a retry mechanism + stream.seek(0) + else: + # If the stream does not support seeking, we need wrap it + # in a BytesIO object. This way we can seek back to the beginning + # of the stream to read the data again if needed. + if not hasattr(httprequest, "_cached_stream"): + httprequest._cached_stream = BytesIO(stream.read()) + stream = httprequest._cached_stream + stream.seek(0) + environ["wsgi.input"] = stream return environ @contextmanager diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index de465a9a7..e7ccf7674 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -55,6 +55,16 @@ class FastapiEndpoint(models.Model): readonly=False, domain="[('user_ids', 'in', user_id)]", ) + save_http_session = fields.Boolean( + string="Save HTTP Session", + help="Whether session should be saved into the session store. This is " + "required if for example you use the Odoo's authentication mechanism. " + "Oherwise chance are high that you don't need it and could turn off " + "this behaviour. Additionaly turning off this option will prevent useless " + "IO operation when storing and reading the session on the disk and prevent " + "unexpecteed disk space consumption.", + default=True, + ) @api.depends("root_path") def _compute_root_path(self): @@ -178,6 +188,7 @@ def _get_routing_info(self): f"{self.root_path}/", f"{self.root_path}/", ], + "save_session": self.save_http_session, # csrf ????? } diff --git a/fastapi/readme/newsfragments/440.bugfix b/fastapi/readme/newsfragments/440.bugfix new file mode 100644 index 000000000..202669d47 --- /dev/null +++ b/fastapi/readme/newsfragments/440.bugfix @@ -0,0 +1,6 @@ +Fix issue with the retry of a POST request with a body content. + +Prior to this fix the retry of a POST request with a body content would +stuck in a loop and never complete. This was due to the fact that the +request input stream was not reset after a failed attempt to process the +request. diff --git a/fastapi/readme/newsfragments/442.feature b/fastapi/readme/newsfragments/442.feature new file mode 100644 index 000000000..f45f73ebf --- /dev/null +++ b/fastapi/readme/newsfragments/442.feature @@ -0,0 +1 @@ +* A new parameter is now available on the endpoint model to let you disable the creation and the store of session files used by Odoo for calls to your application endpoint. This is usefull to prevent disk space consumption and IO operations if your application doesn't need to use this sessions files which are mainly used by Odoo by to store the session info of logged in users. diff --git a/fastapi/routers/demo_router.py b/fastapi/routers/demo_router.py index e316d8dfb..01e9eef1d 100644 --- a/fastapi/routers/demo_router.py +++ b/fastapi/routers/demo_router.py @@ -15,7 +15,8 @@ from odoo.addons.base.models.res_partner import Partner -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, File, HTTPException, Query, status +from fastapi.responses import JSONResponse from ..dependencies import authenticated_partner, fastapi_endpoint, odoo_env from ..models import FastapiEndpoint @@ -95,7 +96,7 @@ async def endpoint_app_info( @router.get("/demo/retrying") async def retrying( - nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)] + nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)], ) -> int: """This method is used in the test suite to check that the retrying functionality in case of concurrency error on the database is working @@ -114,6 +115,28 @@ async def retrying( return tryno +@router.post("/demo/retrying") +async def retrying_post( + nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)], + file: Annotated[bytes, File()], +) -> JSONResponse: + """This method is used in the test suite to check that the retrying + functionality in case of concurrency error on the database is working + correctly for retryable exceptions. + + The output will be the number of retries that have been done. + + This method is mainly used to test the retrying functionality + """ + global _CPT + if _CPT < nbr_retries: + _CPT += 1 + raise FakeConcurrentUpdateError("fake error") + tryno = _CPT + _CPT = 0 + return JSONResponse(content={"retries": tryno, "file": file.decode("utf-8")}) + + class FakeConcurrentUpdateError(OperationalError): @property def pgcode(self): diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index dcc7385b6..37b11a961 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -63,6 +63,18 @@ def test_retrying(self): self.assertEqual(response.status_code, 200) self.assertEqual(int(response.content), nbr_retries) + def test_retrying_post(self): + """Test that the retrying mechanism is working as expected with the + FastAPI endpoints in case of POST request with a file. + """ + nbr_retries = 3 + route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + response = self.url_open( + route, timeout=20, files={"file": ("test.txt", b"test")} + ) + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"}) + @mute_logger("odoo.http") def assert_exception_processed( self, diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index 5b0d0a46d..1f1516be3 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -46,6 +46,7 @@ + diff --git a/requirements.txt b/requirements.txt index abc1ab723..bcba9b734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # generated from manifests external_dependencies -a2wsgi -fastapi +a2wsgi>=1.10.6 +fastapi>=0.110.0 parse-accept-language python-multipart ujson