@@ -898,6 +898,160 @@ async fn test_large_compressible_object_no_compression() {
898898 assert_eq ! ( body, content) ;
899899}
900900
901+ /// Regression test: PUT with HTTP chunked transfer-encoded body must be decoded.
902+ ///
903+ /// AWS CLI sends request bodies with chunked transfer encoding that a reverse
904+ /// proxy (nginx) may forward without decoding. Without decoding, the chunked
905+ /// framing (e.g., "100000\r\n") gets stored as part of the object data,
906+ /// corrupting the file.
907+ #[ tokio:: test]
908+ async fn test_put_object_decodes_chunked_body ( ) {
909+ let ( base_url, _) = spawn_server ( false ) . await ;
910+ let client = Client :: builder ( )
911+ . no_gzip ( )
912+ . no_brotli ( )
913+ . no_deflate ( )
914+ . build ( )
915+ . unwrap ( ) ;
916+
917+ let bucket = "chunked-decode-bucket" ;
918+ let key = "photo.jpg" ;
919+
920+ // Simulate a JPEG file (starts with FF D8 FF)
921+ let original_data: Vec < u8 > = {
922+ let mut v = vec ! [ 0xFF , 0xD8 , 0xFF , 0xE0 ] ;
923+ v. extend_from_slice ( & [ 0x42 ; 2044 ] ) ; // 2048 bytes total
924+ v
925+ } ;
926+
927+ // Wrap the data in HTTP chunked transfer encoding framing
928+ // This simulates what happens when nginx strips Transfer-Encoding header
929+ // but forwards the raw chunked body
930+ let chunk_size_hex = format ! ( "{:x}" , original_data. len( ) ) ;
931+ let mut chunked_body: Vec < u8 > = Vec :: new ( ) ;
932+ chunked_body. extend_from_slice ( chunk_size_hex. as_bytes ( ) ) ; // "800"
933+ chunked_body. extend_from_slice ( b"\r \n " ) ;
934+ chunked_body. extend_from_slice ( & original_data) ;
935+ chunked_body. extend_from_slice ( b"\r \n " ) ;
936+ chunked_body. extend_from_slice ( b"0\r \n \r \n " ) ; // Terminal chunk
937+
938+ assert_ne ! ( chunked_body. len( ) , original_data. len( ) , "Chunked body should be larger" ) ;
939+
940+ // Create bucket
941+ client. put ( & format ! ( "{}/{}" , base_url, bucket) ) . send ( ) . await . unwrap ( ) ;
942+
943+ // Upload the chunked-encoded body (simulating broken proxy behavior)
944+ let res = client. put ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
945+ . body ( chunked_body)
946+ . header ( "Content-Type" , "image/jpeg" )
947+ . send ( )
948+ . await
949+ . unwrap ( ) ;
950+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
951+
952+ // Download and verify the gateway decoded the chunked framing
953+ let res = client. get ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
954+ . send ( )
955+ . await
956+ . unwrap ( ) ;
957+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
958+
959+ let body = res. bytes ( ) . await . unwrap ( ) ;
960+ assert_eq ! (
961+ body. len( ) , original_data. len( ) ,
962+ "Downloaded size ({}) should match original ({}), not chunked body" ,
963+ body. len( ) , original_data. len( )
964+ ) ;
965+ assert_eq ! (
966+ & body[ ..4 ] , & [ 0xFF , 0xD8 , 0xFF , 0xE0 ] ,
967+ "Body must start with JPEG magic, not chunked framing"
968+ ) ;
969+ assert_eq ! ( body. as_ref( ) , original_data. as_slice( ) , "Decoded body must match original" ) ;
970+ }
971+
972+ /// Test that chunked decoding handles aws-chunked format (with chunk-signature extensions)
973+ #[ tokio:: test]
974+ async fn test_put_object_decodes_aws_chunked_body ( ) {
975+ let ( base_url, _) = spawn_server ( false ) . await ;
976+ let client = Client :: builder ( )
977+ . no_gzip ( )
978+ . no_brotli ( )
979+ . no_deflate ( )
980+ . build ( )
981+ . unwrap ( ) ;
982+
983+ let bucket = "aws-chunked-bucket" ;
984+ let key = "data.bin" ;
985+ let original_data = b"Hello, this is test data for aws-chunked decoding!" ;
986+
987+ // Build aws-chunked encoded body:
988+ // <hex-size>;chunk-signature=<fake-sig>\r\n<data>\r\n0;chunk-signature=<fake-sig>\r\n\r\n
989+ let fake_sig = "a" . repeat ( 64 ) ;
990+ let mut chunked_body: Vec < u8 > = Vec :: new ( ) ;
991+ chunked_body. extend_from_slice ( format ! ( "{:x};chunk-signature={}\r \n " , original_data. len( ) , fake_sig) . as_bytes ( ) ) ;
992+ chunked_body. extend_from_slice ( original_data) ;
993+ chunked_body. extend_from_slice ( b"\r \n " ) ;
994+ chunked_body. extend_from_slice ( format ! ( "0;chunk-signature={}\r \n \r \n " , fake_sig) . as_bytes ( ) ) ;
995+
996+ // Create bucket
997+ client. put ( & format ! ( "{}/{}" , base_url, bucket) ) . send ( ) . await . unwrap ( ) ;
998+
999+ // Upload with x-amz-decoded-content-length header (AWS streaming signature signal)
1000+ let res = client. put ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
1001+ . body ( chunked_body)
1002+ . header ( "Content-Encoding" , "aws-chunked" )
1003+ . header ( "x-amz-decoded-content-length" , original_data. len ( ) . to_string ( ) )
1004+ . send ( )
1005+ . await
1006+ . unwrap ( ) ;
1007+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
1008+
1009+ // Download and verify
1010+ let res = client. get ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
1011+ . send ( )
1012+ . await
1013+ . unwrap ( ) ;
1014+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
1015+
1016+ let body = res. bytes ( ) . await . unwrap ( ) ;
1017+ assert_eq ! ( body. as_ref( ) , original_data, "Body must be decoded aws-chunked content" ) ;
1018+ }
1019+
1020+ /// Test that normal (non-chunked) uploads are NOT affected by chunked decoding
1021+ #[ tokio:: test]
1022+ async fn test_put_object_normal_body_unaffected ( ) {
1023+ let ( base_url, _) = spawn_server ( false ) . await ;
1024+ let client = Client :: new ( ) ;
1025+
1026+ let bucket = "normal-body-bucket" ;
1027+ let key = "binary.dat" ;
1028+
1029+ // Binary data that could theoretically look like a chunk header if we're not careful
1030+ // (starts with bytes that are valid hex chars in ASCII)
1031+ let original_data: Vec < u8 > = vec ! [ 0x41 , 0x42 , 0x43 , 0x44 , 0x45 , 0x46 ] ; // "ABCDEF" in ASCII
1032+
1033+ // Create bucket
1034+ client. put ( & format ! ( "{}/{}" , base_url, bucket) ) . send ( ) . await . unwrap ( ) ;
1035+
1036+ // Upload normally
1037+ let res = client. put ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
1038+ . body ( original_data. clone ( ) )
1039+ . send ( )
1040+ . await
1041+ . unwrap ( ) ;
1042+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
1043+
1044+ // Download - should be identical
1045+ let res = client. get ( & format ! ( "{}/{}/{}" , base_url, bucket, key) )
1046+ . send ( )
1047+ . await
1048+ . unwrap ( ) ;
1049+ assert_eq ! ( res. status( ) , StatusCode :: OK ) ;
1050+
1051+ let body = res. bytes ( ) . await . unwrap ( ) ;
1052+ assert_eq ! ( body. as_ref( ) , original_data. as_slice( ) , "Normal upload must not be modified" ) ;
1053+ }
1054+
9011055/// Test duplicate bucket creation for same user fails
9021056#[ tokio:: test]
9031057async fn test_multitenant_duplicate_bucket_same_user ( ) {
0 commit comments