@@ -20,7 +20,7 @@ use ::tokio::runtime::Builder;
2020use :: vespera:: axum:: Json ;
2121use :: vespera:: multipart:: { FieldData , TypedMultipart } ;
2222use :: vespera:: tempfile:: NamedTempFile ;
23- use :: vespera:: { Multipart , Schema } ;
23+ use :: vespera:: { Multipart , Schema , Validated } ;
2424use :: vespera_inprocess:: { dispatch_from_bytes, register_app} ;
2525
2626#[ derive( Multipart , Schema ) ]
@@ -34,6 +34,20 @@ struct UploadReq {
3434 file : FieldData < NamedTempFile > ,
3535}
3636
37+ #[ derive( Multipart , Schema ) ]
38+ #[ allow( dead_code) ]
39+ struct CappedUploadReq {
40+ name : String ,
41+ file : FieldData < NamedTempFile > ,
42+ }
43+
44+ #[ derive( Multipart , Schema , garde:: Validate ) ]
45+ #[ allow( dead_code) ]
46+ struct ValidatedMultipartReq {
47+ #[ garde( length( min = 3 ) ) ]
48+ name : String ,
49+ }
50+
3751#[ derive( Serialize , Schema ) ]
3852struct UploadResult {
3953 name : String ,
@@ -59,6 +73,29 @@ async fn upload_handler(TypedMultipart(mut req): TypedMultipart<UploadReq>) -> J
5973 } )
6074}
6175
76+ async fn capped_upload_handler (
77+ TypedMultipart ( mut req) : TypedMultipart < CappedUploadReq > ,
78+ ) -> Json < UploadResult > {
79+ let mut buf = Vec :: new ( ) ;
80+ let f = req. file . contents . as_file_mut ( ) ;
81+ f. seek ( SeekFrom :: Start ( 0 ) ) . expect ( "rewind temp file" ) ;
82+ f. read_to_end ( & mut buf) . expect ( "read temp file" ) ;
83+ Json ( UploadResult {
84+ name : req. name ,
85+ file_size : u64:: try_from ( buf. len ( ) ) . expect ( "file size fits in u64" ) ,
86+ file_first_byte : * buf. first ( ) . unwrap_or ( & 0 ) ,
87+ file_last_byte : * buf. last ( ) . unwrap_or ( & 0 ) ,
88+ } )
89+ }
90+
91+ async fn validated_multipart_handler (
92+ Validated ( TypedMultipart ( req) ) : Validated < TypedMultipart < ValidatedMultipartReq > > ,
93+ ) -> Json < TextResult > {
94+ Json ( TextResult {
95+ text_len : u64:: try_from ( req. name . len ( ) ) . unwrap_or ( u64:: MAX ) ,
96+ } )
97+ }
98+
6299/// Unannotated `String` field — inherits the default 1 MiB cap.
63100#[ derive( Multipart , Schema ) ]
64101#[ allow( dead_code) ]
@@ -96,6 +133,8 @@ async fn text_unlimited_handler(
96133fn multipart_router ( ) -> Router {
97134 Router :: new ( )
98135 . route ( "/upload" , post ( upload_handler) )
136+ . route ( "/capped-upload" , post ( capped_upload_handler) )
137+ . route ( "/validated-multipart" , post ( validated_multipart_handler) )
99138 . route ( "/text" , post ( text_handler) )
100139 . route ( "/text-unlimited" , post ( text_unlimited_handler) )
101140 // Disable the 2 MiB default so the 256 KiB test below isn't
@@ -116,6 +155,16 @@ fn encode_multipart_wire(
116155 name : & str ,
117156 file_name : & str ,
118157 file_bytes : & [ u8 ] ,
158+ ) -> Vec < u8 > {
159+ encode_multipart_upload_wire ( "/upload" , boundary, name, file_name, file_bytes)
160+ }
161+
162+ fn encode_multipart_upload_wire (
163+ path : & str ,
164+ boundary : & str ,
165+ name : & str ,
166+ file_name : & str ,
167+ file_bytes : & [ u8 ] ,
119168) -> Vec < u8 > {
120169 let mut body = Vec :: new ( ) ;
121170 body. extend_from_slice ( format ! ( "--{boundary}\r \n " ) . as_bytes ( ) ) ;
@@ -139,7 +188,7 @@ fn encode_multipart_wire(
139188 let header_json = :: serde_json:: json!( {
140189 "v" : 1 ,
141190 "method" : "POST" ,
142- "path" : "/upload" ,
191+ "path" : path ,
143192 "headers" : headers,
144193 } ) ;
145194 let header_bytes = :: serde_json:: to_vec ( & header_json) . expect ( "header serialise" ) ;
@@ -341,3 +390,53 @@ fn string_field_unlimited_optout_allows_large() {
341390 let json: Value = :: serde_json:: from_slice ( & body) . expect ( "response is JSON" ) ;
342391 assert_eq ! ( json[ "text_len" ] . as_u64( ) , Some ( 1024 * 1024 + 1 ) ) ;
343392}
393+
394+ #[ test]
395+ fn named_temp_file_over_default_cap_rejected_413 ( ) {
396+ install_router_once ( ) ;
397+ let runtime = Builder :: new_current_thread ( )
398+ . enable_all ( )
399+ . build ( )
400+ . expect ( "tokio runtime" ) ;
401+
402+ let payload = vec ! [ b'z' ; 1024 * 1024 + 1 ] ;
403+ let wire = encode_multipart_upload_wire (
404+ "/capped-upload" ,
405+ "----TempFileCapBoundary" ,
406+ "bob" ,
407+ "too-large.bin" ,
408+ & payload,
409+ ) ;
410+ let resp = dispatch_from_bytes ( wire, & runtime) ;
411+ let ( header, _body) = decode_wire ( & resp) ;
412+ assert_eq ! (
413+ header[ "status" ] . as_u64( ) ,
414+ Some ( 413 ) ,
415+ "oversized unannotated tempfile field must be rejected with 413, got header={header:#}"
416+ ) ;
417+ }
418+
419+ #[ test]
420+ fn validated_typed_multipart_rejects_garde_failure_422 ( ) {
421+ install_router_once ( ) ;
422+ let runtime = Builder :: new_current_thread ( )
423+ . enable_all ( )
424+ . build ( )
425+ . expect ( "tokio runtime" ) ;
426+
427+ let wire = encode_multipart_text (
428+ "----ValidatedMultipartBoundary" ,
429+ "/validated-multipart" ,
430+ "name" ,
431+ b"xy" ,
432+ ) ;
433+ let resp = dispatch_from_bytes ( wire, & runtime) ;
434+ let ( header, body) = decode_wire ( & resp) ;
435+ assert_eq ! (
436+ header[ "status" ] . as_u64( ) ,
437+ Some ( 422 ) ,
438+ "garde failure must be rejected with 422, got header={header:#}"
439+ ) ;
440+ let json: Value = :: serde_json:: from_slice ( & body) . expect ( "response is JSON" ) ;
441+ assert_eq ! ( json[ "errors" ] [ 0 ] [ "path" ] , "name" ) ;
442+ }
0 commit comments