Compare commits

...

3 Commits

Author SHA1 Message Date
Sebastián Ramírez
784f06cb9b 🔖 Release version 0.117.1 2025-09-20 22:15:41 +02:00
github-actions[bot]
b5c05893b4 📝 Update release notes
[skip ci]
2025-09-20 19:56:30 +00:00
Thomas LÉVEIL
44fc67632b 🐛 Fix validation error when File is declared after Form parameter (#11194)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2025-09-20 21:55:59 +02:00
4 changed files with 100 additions and 5 deletions

View File

@@ -7,6 +7,12 @@ hide:
## Latest Changes
## 0.117.1
### Fixes
* 🐛 Fix validation error when `File` is declared after `Form` parameter. PR [#11194](https://github.com/fastapi/fastapi/pull/11194) by [@thomasleveil](https://github.com/thomasleveil).
## 0.117.0
### Features

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.117.0"
__version__ = "0.117.1"
from starlette import status as status

View 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

View File

@@ -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"}