@@ -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