]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix validation error when `File` is declared after `Form` parameter (#11194)
authorThomas LÉVEIL <thomasleveil@users.noreply.github.com>
Sat, 20 Sep 2025 19:55:59 +0000 (21:55 +0200)
committerGitHub <noreply@github.com>
Sat, 20 Sep 2025 19:55:59 +0000 (21:55 +0200)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
fastapi/dependencies/utils.py
tests/test_file_and_form_order_issue_9116.py [new file with mode: 0644]

index dc663f5489fc701ed0b0d6e555d0cdcfc16b121f..e49380cb30028260d6f0aa841ef7156324b7ee75 100644 (file)
@@ -872,20 +872,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
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 (file)
index 0000000..cb9a31d
--- /dev/null
@@ -0,0 +1,90 @@
+"""
+Regression test, Error 422 if Form is declared before File
+See https://github.com/tiangolo/fastapi/discussions/9116
+"""
+
+from pathlib import Path
+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: Path) -> Path:
+    f = tmp_path / "example1.txt"
+    f.write_text("foo")
+    return f
+
+
+@pytest.fixture
+def tmp_file_2(tmp_path: Path) -> 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: Path):
+    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: Path, tmp_file_2: Path):
+    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"}