From ed608b6c951eadbd83191c83e5c17ce4747b26d8 Mon Sep 17 00:00:00 2001 From: Valentyn Druzhynin Date: Wed, 4 Feb 2026 23:04:54 -0500 Subject: [PATCH 1/2] 14811: Fix the order for the submitted form data --- fastapi/dependencies/utils.py | 12 +++-- ..._bytes_file_order_preserved_issue_14811.py | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/test_list_bytes_file_order_preserved_issue_14811.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index fc5dfed85a..16ed57f3c7 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -893,17 +893,21 @@ async def _extract_form_body( ): # For types assert isinstance(value, sequence_types) - results: list[Union[bytes, str]] = [] + results: list[Union[bytes, str, None]] = [None] * len(value) async def process_fn( + idx: int, fn: Callable[[], Coroutine[Any, Any, Any]], ) -> None: result = await fn() - results.append(result) # noqa: B023 + # Using index to preserve order + results[idx] = result # noqa: B023 async with anyio.create_task_group() as tg: - for sub_value in value: - tg.start_soon(process_fn, sub_value.read) + for idx, sub_value in enumerate(value): + tg.start_soon(process_fn, idx, sub_value.read) + + assert all(item is not None for item in results) value = serialize_sequence_value(field=field, value=results) if value is not None: values[get_validation_alias(field)] = value diff --git a/tests/test_list_bytes_file_order_preserved_issue_14811.py b/tests/test_list_bytes_file_order_preserved_issue_14811.py new file mode 100644 index 0000000000..7879b49cd5 --- /dev/null +++ b/tests/test_list_bytes_file_order_preserved_issue_14811.py @@ -0,0 +1,46 @@ +""" +Regression test: preserve order when using list[bytes] + File() +See https://github.com/fastapi/fastapi/discussions/14811 +Related: PR #3372 +""" + +from typing import Annotated + +import anyio +import pytest +from fastapi import FastAPI, File +from fastapi.testclient import TestClient +from starlette.datastructures import UploadFile as StarletteUploadFile + + +def test_list_bytes_file_preserves_order_issue_14811( + monkeypatch: pytest.MonkeyPatch, +) -> None: + app = FastAPI() + + @app.post("/upload") + async def upload(files: Annotated[list[bytes], File()]): + # return something that makes order obvious + return [b[0] for b in files] + + original_read = StarletteUploadFile.read + + async def patched_read(self: StarletteUploadFile, size: int = -1) -> bytes: + # Make the FIRST file slower *deterministically* + if self.filename == "slow.txt": + await anyio.sleep(0.05) + return await original_read(self, size) + + monkeypatch.setattr(StarletteUploadFile, "read", patched_read) + + client = TestClient(app) + + files = [ + ("files", ("slow.txt", b"A" * 10, "text/plain")), + ("files", ("fast.txt", b"B" * 10, "text/plain")), + ] + r = client.post("/upload", files=files) + assert r.status_code == 200, r.text + + # Must preserve request order: slow first, fast second + assert r.json() == [ord("A"), ord("B")] From 121e332322d65f8b2bb42a319f13869ef9c44444 Mon Sep 17 00:00:00 2001 From: Valentyn Date: Thu, 5 Feb 2026 17:47:43 +0200 Subject: [PATCH 2/2] Update tests/test_list_bytes_file_order_preserved_issue_14811.py Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- tests/test_list_bytes_file_order_preserved_issue_14811.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_list_bytes_file_order_preserved_issue_14811.py b/tests/test_list_bytes_file_order_preserved_issue_14811.py index 7879b49cd5..399235bdbf 100644 --- a/tests/test_list_bytes_file_order_preserved_issue_14811.py +++ b/tests/test_list_bytes_file_order_preserved_issue_14811.py @@ -13,7 +13,7 @@ from fastapi.testclient import TestClient from starlette.datastructures import UploadFile as StarletteUploadFile -def test_list_bytes_file_preserves_order_issue_14811( +def test_list_bytes_file_preserves_order( monkeypatch: pytest.MonkeyPatch, ) -> None: app = FastAPI()