Skip to content

Commit 5082765

Browse files
Merge branch 'main' into support-opentelemetry-python-1-40
2 parents cd10c61 + ab72cc5 commit 5082765

File tree

3 files changed

+41
-99
lines changed

3 files changed

+41
-99
lines changed

.github/workflows/cherry-pick-v2.yaml

Lines changed: 0 additions & 99 deletions
This file was deleted.

litestar/_kwargs/extractors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ async def _extract_multipart(
371371

372372
for name, tp in field_definition.get_type_hints().items():
373373
value = form_values.get(name)
374+
if value == "" and is_optional_union(tp):
375+
inner: Any = make_non_optional_union(tp)
376+
if isinstance(inner, type) and issubclass(inner, UploadFile):
377+
form_values[name] = None # pyright: ignore
378+
continue
374379
if (
375380
value is not None
376381
and not isinstance(value, list)

tests/unit/test_kwargs/test_multipart_data.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,39 @@ async def consistency_handler(request: Request) -> str:
652652

653653
# Results should be identical
654654
assert response_url.text == response_multipart.text
655+
656+
657+
# https://github.com/litestar-org/litestar/issues/4647
658+
def test_optional_upload_file_without_file_submitted() -> None:
659+
"""Optional[UploadFile] in a dataclass should be None when no file is submitted via multipart."""
660+
661+
@dataclass
662+
class UploadForm:
663+
file: Optional[UploadFile] = None
664+
665+
@post("/", signature_namespace={"UploadForm": UploadForm, "UploadFile": UploadFile})
666+
async def handler(
667+
data: Annotated[UploadForm, Body(media_type=RequestEncodingType.MULTI_PART)],
668+
) -> str:
669+
return "none" if data.file is None else "file"
670+
671+
with create_test_client([handler]) as client:
672+
# Simulate browser submitting form without selecting a file (sends filename="" with empty body)
673+
response = client.post(
674+
"/",
675+
content=(
676+
b"--testboundary\r\n"
677+
b'Content-Disposition: form-data; name="file"; filename=""\r\n'
678+
b"Content-Type: application/octet-stream\r\n\r\n"
679+
b"\r\n"
680+
b"--testboundary--\r\n"
681+
),
682+
headers={"Content-Type": "multipart/form-data; boundary=testboundary"},
683+
)
684+
assert response.status_code == HTTP_201_CREATED
685+
assert response.text == "none"
686+
687+
# Submitting with an actual file should still work
688+
response = client.post("/", files={"file": ("test.txt", b"hello", "text/plain")})
689+
assert response.status_code == HTTP_201_CREATED
690+
assert response.text == "file"

0 commit comments

Comments
 (0)