From 7e92b2b46db1e0eb35020ff86c07fdb8a193291e Mon Sep 17 00:00:00 2001 From: Thomas LEVEIL Date: Sun, 23 Aug 2020 22:57:10 +0200 Subject: [PATCH 1/2] :white_check_mark: add tests for using `Form` and `File` in the same endpoint --- tests/test_file_and_form_order_issue_9116.py | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/test_file_and_form_order_issue_9116.py diff --git a/tests/test_file_and_form_order_issue_9116.py b/tests/test_file_and_form_order_issue_9116.py new file mode 100644 index 000000000..a1dde9f69 --- /dev/null +++ b/tests/test_file_and_form_order_issue_9116.py @@ -0,0 +1,92 @@ +""" +See https://github.com/tiangolo/fastapi/discussions/9116 + +Showcases regression introduced in commit ab2b86f + +""" + +import pathlib +from typing import List + +import pytest +from fastapi import FastAPI, File, Form +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/file_before_form") +def file_before_form( + file: bytes = File(...), + city: str = Form(...), +): + return {"file_content": file, "city": city} + + +@app.post("/file_after_form") +def file_after_form( + city: str = Form(...), + file: bytes = File(...), +): + return {"file_content": file, "city": city} + + +@app.post("/file_list_before_form") +def file_list_before_form( + files: Annotated[List[bytes], File()], + city: Annotated[str, Form(...)], +): + return {"file_contents": files, "city": city} + + +@app.post("/file_list_after_form") +def file_list_after_form( + city: Annotated[str, Form(...)], + files: Annotated[List[bytes], File()], +): + return {"file_contents": files, "city": city} + + +client = TestClient(app) + + +@pytest.fixture +def tmp_file_1(tmp_path) -> pathlib.Path: + f = tmp_path / "example1.txt" + f.write_text("foo") + return f + + +@pytest.fixture +def tmp_file_2(tmp_path) -> pathlib.Path: + f = tmp_path / "example2.txt" + f.write_text("bar") + return f + + +@pytest.mark.parametrize("endpoint_path", ("/file_before_form", "/file_after_form")) +def test_file_form_order(endpoint_path: str, tmp_file_1): + response = client.post( + url=endpoint_path, + data={"city": "Thimphou"}, + files={"file": (tmp_file_1.name, tmp_file_1.read_bytes())}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_content": "foo", "city": "Thimphou"} + + +@pytest.mark.parametrize( + "endpoint_path", ("/file_list_before_form", "/file_list_after_form") +) +def test_file_list_form_order(endpoint_path: str, tmp_file_1, tmp_file_2): + response = client.post( + url=endpoint_path, + data={"city": "Thimphou"}, + files=( + ("files", (tmp_file_1.name, tmp_file_1.read_bytes())), + ("files", (tmp_file_2.name, tmp_file_2.read_bytes())), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_contents": ["foo", "bar"], "city": "Thimphou"} From 6e0abe9459842daddaa78d85fcf584b3a43cfe81 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 22 Jun 2025 19:19:50 +0200 Subject: [PATCH 2/2] :bug: Fix handling uploaded file data correctly when not in the first part of a multipart/form-data request body The HTTP specification for multipart/form-data, defined in RFC 7578, allows both form data and uploaded files in a single request. It does not require files to be before or after form fields. As such, no specific care is to be given to the first field. References: - [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) - [FastAPI issue 9116](https://github.com/fastapi/fastapi/discussions/9116) Close #9116 --- fastapi/dependencies/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 84dfa4d03..947ff2244 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -843,20 +843,19 @@ async def _extract_form_body( received_body: FormData, ) -> Dict[str, Any]: values = {} - first_field = body_fields[0] - first_field_info = first_field.field_info for field in body_fields: value = _get_multidict_value(field, received_body) + field_info = field.field_info if ( - isinstance(first_field_info, params.File) + isinstance(field_info, params.File) and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( is_bytes_sequence_field(field) - and isinstance(first_field_info, params.File) + and isinstance(field_info, params.File) and value_is_sequence(value) ): # For types