diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index dd42371ecc..f3841a70fa 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -900,17 +900,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..399235bdbf --- /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( + 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")]