@@ -144,7 +144,7 @@ async fn build_http_request(
144144 auth_method : & AuthMethod ,
145145 page_token : Option < & str > ,
146146 pages_fetched : u32 ,
147- upload_path : Option < & str > ,
147+ upload_bytes : Option < & [ u8 ] > ,
148148) -> Result < reqwest:: RequestBuilder , GwsError > {
149149 let mut request = match method. http_method . as_str ( ) {
150150 "GET" => client. get ( & input. full_url ) ,
@@ -180,17 +180,11 @@ async fn build_http_request(
180180
181181 if pages_fetched == 0 {
182182 if input. is_upload {
183- let upload_path = upload_path. expect ( "upload_path must be Some when is_upload is true" ) ;
184-
185- let file_bytes = tokio:: fs:: read ( upload_path) . await . map_err ( |e| {
186- GwsError :: Validation ( format ! (
187- "Failed to read upload file '{}': {}" ,
188- upload_path, e
189- ) )
190- } ) ?;
183+ let file_bytes =
184+ upload_bytes. expect ( "upload bytes must be Some when is_upload is true" ) ;
191185
192186 request = request. query ( & [ ( "uploadType" , "multipart" ) ] ) ;
193- let ( multipart_body, content_type) = build_multipart_body ( & input. body , & file_bytes) ?;
187+ let ( multipart_body, content_type) = build_multipart_body ( & input. body , file_bytes) ?;
194188 request = request. header ( "Content-Type" , content_type) ;
195189 request = request. body ( multipart_body) ;
196190 } else if let Some ( ref body_val) = input. body {
@@ -307,17 +301,27 @@ async fn handle_binary_response(
307301 output_format : & crate :: formatter:: OutputFormat ,
308302 capture_output : bool ,
309303) -> Result < Option < Value > , GwsError > {
310- let file_path = if let Some ( p) = output_path {
311- PathBuf :: from ( p)
304+ let ( mut file, file_path) = if let Some ( path) = output_path {
305+ let path_owned = path. to_string ( ) ;
306+ let ( file, canonical_path) = tokio:: task:: spawn_blocking ( move || {
307+ crate :: validate:: open_safe_output_file ( & path_owned)
308+ } )
309+ . await
310+ . map_err ( |e| {
311+ GwsError :: Other ( anyhow:: anyhow!(
312+ "Output file open/validation task failed: {e}"
313+ ) )
314+ } ) ??;
315+ ( tokio:: fs:: File :: from_std ( file) , canonical_path)
312316 } else {
313317 let ext = mime_to_extension ( content_type) ;
314- PathBuf :: from ( format ! ( "download.{ext}" ) )
318+ let file_path = PathBuf :: from ( format ! ( "download.{ext}" ) ) ;
319+ let file = tokio:: fs:: File :: create ( & file_path)
320+ . await
321+ . context ( "Failed to create output file" ) ?;
322+ ( file, file_path)
315323 } ;
316324
317- let mut file = tokio:: fs:: File :: create ( & file_path)
318- . await
319- . context ( "Failed to create output file" ) ?;
320-
321325 let mut stream = response. bytes_stream ( ) ;
322326 let mut total_bytes: u64 = 0 ;
323327
@@ -377,24 +381,24 @@ pub async fn execute_method(
377381 // Validate untrusted filesystem paths before any network or file I/O.
378382 let safe_output_path = if let Some ( path) = output_path {
379383 let path_owned = path. to_string ( ) ;
380- let validated = tokio:: task:: spawn_blocking ( move || {
384+ tokio:: task:: spawn_blocking ( move || {
381385 crate :: validate:: validate_safe_output_file_path ( & path_owned)
382386 } )
383387 . await
384388 . map_err ( |e| GwsError :: Other ( anyhow:: anyhow!( "Path validation task failed: {e}" ) ) ) ??;
385- Some ( validated . to_string_lossy ( ) . into_owned ( ) )
389+ Some ( path . to_string ( ) )
386390 } else {
387391 None
388392 } ;
389393
390394 let safe_upload_path = if let Some ( path) = upload_path {
391395 let path_owned = path. to_string ( ) ;
392- let validated = tokio:: task:: spawn_blocking ( move || {
396+ tokio:: task:: spawn_blocking ( move || {
393397 crate :: validate:: validate_safe_upload_file_path ( & path_owned)
394398 } )
395399 . await
396400 . map_err ( |e| GwsError :: Other ( anyhow:: anyhow!( "Path validation task failed: {e}" ) ) ) ??;
397- Some ( validated . to_string_lossy ( ) . into_owned ( ) )
401+ Some ( path . to_string ( ) )
398402 } else {
399403 None
400404 } ;
@@ -426,6 +430,21 @@ pub async fn execute_method(
426430 return Ok ( None ) ;
427431 }
428432
433+ let upload_bytes = if input. is_upload {
434+ let upload_path = safe_upload_path
435+ . as_deref ( )
436+ . expect ( "safe_upload_path must be Some when is_upload is true" )
437+ . to_string ( ) ;
438+ let bytes = tokio:: task:: spawn_blocking ( move || {
439+ crate :: validate:: read_safe_upload_file ( & upload_path)
440+ } )
441+ . await
442+ . map_err ( |e| GwsError :: Other ( anyhow:: anyhow!( "Upload file read task failed: {e}" ) ) ) ??;
443+ Some ( bytes)
444+ } else {
445+ None
446+ } ;
447+
429448 let mut page_token: Option < String > = None ;
430449 let mut pages_fetched: u32 = 0 ;
431450 let mut captured_values = Vec :: new ( ) ;
@@ -440,7 +459,7 @@ pub async fn execute_method(
440459 & auth_method,
441460 page_token. as_deref ( ) ,
442461 pages_fetched,
443- safe_upload_path . as_deref ( ) ,
462+ upload_bytes . as_deref ( ) ,
444463 )
445464 . await ?;
446465
@@ -2071,3 +2090,47 @@ async fn test_get_does_not_set_content_length_zero() {
20712090 "GET with no body should not have Content-Length header"
20722091 ) ;
20732092}
2093+
2094+ #[ tokio:: test]
2095+ async fn test_build_http_request_upload_uses_provided_bytes ( ) {
2096+ let client = reqwest:: Client :: new ( ) ;
2097+ let method = RestMethod {
2098+ http_method : "POST" . to_string ( ) ,
2099+ path : "files" . to_string ( ) ,
2100+ supports_media_upload : true ,
2101+ ..Default :: default ( )
2102+ } ;
2103+ let input = ExecutionInput {
2104+ full_url : "https://example.com/files" . to_string ( ) ,
2105+ body : Some ( json ! ( { "name" : "x.txt" } ) ) ,
2106+ params : Map :: new ( ) ,
2107+ query_params : HashMap :: new ( ) ,
2108+ is_upload : true ,
2109+ } ;
2110+
2111+ let request = build_http_request (
2112+ & client,
2113+ & method,
2114+ & input,
2115+ None ,
2116+ & AuthMethod :: None ,
2117+ None ,
2118+ 0 ,
2119+ Some ( b"hello" ) ,
2120+ )
2121+ . await
2122+ . unwrap ( ) ;
2123+
2124+ let built = request. build ( ) . unwrap ( ) ;
2125+ assert ! ( built
2126+ . url( )
2127+ . query( )
2128+ . unwrap_or_default( )
2129+ . contains( "uploadType=multipart" ) ) ;
2130+ let content_type = built
2131+ . headers ( )
2132+ . get ( "Content-Type" )
2133+ . and_then ( |v| v. to_str ( ) . ok ( ) )
2134+ . unwrap_or_default ( ) ;
2135+ assert ! ( content_type. starts_with( "multipart/related; boundary=" ) ) ;
2136+ }
0 commit comments