From e201c735aaf373d614f7d7940132cf75c9102772 Mon Sep 17 00:00:00 2001 From: Abhishek Biswas Date: Wed, 22 Oct 2025 00:44:54 +0530 Subject: [PATCH] fix ISSUE 13533 --- fastapi/dependencies/utils.py | 13 ++- .../test_forms_empty_string_default_13533.py | 103 ++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/test_forms_empty_string_default_13533.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index aa06dd2a9..8b080fdb5 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -899,6 +899,8 @@ async def _extract_form_body( received_body: FormData, ) -> Dict[str, Any]: values = {} + # Track which keys are defined as body_fields to avoid re-adding empty strings + body_field_aliases = {field.alias for field in body_fields} for field in body_fields: value = _get_multidict_value(field, received_body) @@ -928,10 +930,17 @@ async def _extract_form_body( for sub_value in value: tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) - if value is not None: + # Don't add empty strings for Form fields - _get_multidict_value already handled them + if value is not None and not ( + isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)) + and isinstance(value, str) + and value == "" + ): values[field.alias] = value + # Only add extra fields that are not already defined as body_fields + # This prevents overwriting correctly processed default values for key, value in received_body.items(): - if key not in values: + if key not in body_field_aliases and key not in values: values[key] = value return values diff --git a/tests/test_forms_empty_string_default_13533.py b/tests/test_forms_empty_string_default_13533.py new file mode 100644 index 000000000..4478b494d --- /dev/null +++ b/tests/test_forms_empty_string_default_13533.py @@ -0,0 +1,103 @@ +""" +Test for issue #13533: Multiple regressions in the handling of forms & form validation + +This test verifies that empty strings in form fields are correctly handled: +- For optional fields with None default, empty string should result in None +- For optional fields with int type, empty string should result in None (not parsing error) +- This applies to both x-www-form-urlencoded and multipart/form-data +""" + +from typing import Annotated, Optional + +from fastapi import FastAPI, File, Form +from fastapi.testclient import TestClient + +# Test app for URL-encoded forms with optional string +app_urlencoded_str = FastAPI() + + +@app_urlencoded_str.post("/test") +def endpoint_urlencoded_str( + name: Annotated[Optional[str], Form(embed=True)] = None, +): + return {"name": name} + + +# Test app for URL-encoded forms with optional int +app_urlencoded_int = FastAPI() + + +@app_urlencoded_int.post("/test") +def endpoint_urlencoded_int( + age: Annotated[Optional[int], Form()] = None, +): + return {"age": age} + + +# Test app for multipart forms +app_multipart = FastAPI() + + +@app_multipart.post("/test") +def endpoint_multipart( + file: Annotated[Optional[bytes], File()] = None, + name: Annotated[Optional[str], Form(embed=True)] = None, +): + return {"file_size": len(file) if file else None, "name": name} + + +def test_urlencoded_empty_string_optional_str(): + """ + Regression test for #13533: Empty string in URL-encoded form + with optional str field should use default value (None) + """ + client = TestClient(app_urlencoded_str) + response = client.post("/test", data={"name": ""}) + assert response.status_code == 200 + assert response.json() == {"name": None} + + +def test_urlencoded_empty_string_optional_int(): + """ + Regression test for #13533: Empty string in URL-encoded form + with optional int field should use default value (None), not cause parsing error + """ + client = TestClient(app_urlencoded_int) + response = client.post("/test", data={"age": ""}) + assert response.status_code == 200 + assert response.json() == {"age": None} + + +def test_multipart_empty_string_optional_str(): + """ + Regression test for #13533: Empty string in multipart form + with optional str field should use default value (None) + """ + client = TestClient(app_multipart) + response = client.post("/test", data={"name": ""}) + assert response.status_code == 200 + assert response.json() == {"file_size": None, "name": None} + + +def test_urlencoded_with_actual_value(): + """Verify that actual values still work correctly""" + client = TestClient(app_urlencoded_str) + response = client.post("/test", data={"name": "John"}) + assert response.status_code == 200 + assert response.json() == {"name": "John"} + + +def test_urlencoded_int_with_actual_value(): + """Verify that actual int values still work correctly""" + client = TestClient(app_urlencoded_int) + response = client.post("/test", data={"age": "25"}) + assert response.status_code == 200 + assert response.json() == {"age": 25} + + +def test_multipart_with_actual_value(): + """Verify that actual values in multipart forms still work correctly""" + client = TestClient(app_multipart) + response = client.post("/test", data={"name": "John"}) + assert response.status_code == 200 + assert response.json() == {"file_size": None, "name": "John"}