@@ -54,23 +54,14 @@ impl HttpClient {
5454 /// * `Err(e)` - If the request fails
5555 pub async fn get_bytes ( & self , url : & str ) -> Result < Vec < u8 > , Error > {
5656 tracing:: debug!( "Fetching bytes from: {}" , url) ;
57- let response = self . get ( url) . await ?;
58- Ok ( response. bytes ( ) . await ?. to_vec ( ) )
59- }
60-
61- async fn get ( & self , url : & str ) -> Result < Response , Error > {
62- self . get_with_accept ( url, None ) . await
63- }
6457
65- async fn get_with_accept ( & self , url : & str , accept : Option < & str > ) -> Result < Response , Error > {
6658 let client = vite_shared:: shared_http_client ( ) ;
6759
68- let response = ( || async {
69- let mut request = client. get ( url) ;
70- if let Some ( accept) = accept {
71- request = request. header ( reqwest:: header:: ACCEPT , accept) ;
72- }
73- request. send ( ) . await ?. error_for_status ( )
60+ // Read the body inside the retry so a mid-body connection drop gets
61+ // retried instead of failing outright, like `download_file`.
62+ let bytes = ( || async {
63+ let response = client. get ( url) . send ( ) . await ?. error_for_status ( ) ?;
64+ Ok :: < _ , Error > ( response. bytes ( ) . await ?)
7465 } )
7566 . retry (
7667 ExponentialBuilder :: default ( )
@@ -80,7 +71,7 @@ impl HttpClient {
8071 )
8172 . await ?;
8273
83- Ok ( response )
74+ Ok ( bytes . to_vec ( ) )
8475 }
8576
8677 /// Get JSON data from a URL
@@ -739,6 +730,69 @@ mod tests {
739730 ) ;
740731 }
741732
733+ /// `get_bytes` used to read the body outside the retry, so a connection
734+ /// dropped mid-body never got retried. The server truncates the first
735+ /// response, then sends the whole body — `get_bytes` should retry and succeed.
736+ #[ tokio:: test]
737+ async fn test_get_bytes_retries_on_truncated_body ( ) {
738+ use std:: sync:: {
739+ Arc ,
740+ atomic:: { AtomicUsize , Ordering } ,
741+ } ;
742+
743+ use tokio:: {
744+ io:: { AsyncReadExt , AsyncWriteExt } ,
745+ net:: TcpListener ,
746+ } ;
747+
748+ let body = b"the complete body payload that must arrive intact" ;
749+ let len = body. len ( ) ;
750+
751+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
752+ let addr = listener. local_addr ( ) . unwrap ( ) ;
753+ let connections = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
754+
755+ let server_connections = Arc :: clone ( & connections) ;
756+ let server = tokio:: spawn ( async move {
757+ loop {
758+ let ( mut socket, _) = listener. accept ( ) . await . unwrap ( ) ;
759+ let attempt = server_connections. fetch_add ( 1 , Ordering :: SeqCst ) ;
760+
761+ // Drain the request before replying.
762+ let mut scratch = [ 0u8 ; 1024 ] ;
763+ let _ = socket. read ( & mut scratch) . await ;
764+
765+ let head = format ! ( "HTTP/1.1 200 OK\r \n Content-Length: {len}\r \n \r \n " ) ;
766+ socket. write_all ( head. as_bytes ( ) ) . await . unwrap ( ) ;
767+ if attempt == 0 {
768+ // First attempt: send half the body, then drop the connection.
769+ socket. write_all ( & body[ ..len / 2 ] ) . await . unwrap ( ) ;
770+ } else {
771+ // Retry: send the whole body.
772+ socket. write_all ( body) . await . unwrap ( ) ;
773+ }
774+ socket. flush ( ) . await . unwrap ( ) ;
775+ }
776+ } ) ;
777+
778+ let client = HttpClient :: with_config ( 3 , 10 ) ;
779+ let url = format ! ( "http://{addr}/" ) ;
780+ let result = client. get_bytes ( & url) . await ;
781+
782+ server. abort ( ) ;
783+
784+ let attempts = connections. load ( Ordering :: SeqCst ) ;
785+ assert ! (
786+ result. is_ok( ) ,
787+ "get_bytes must retry a truncated body and eventually succeed, but got {result:?} after {attempts} attempt(s)"
788+ ) ;
789+ assert_eq ! ( result. unwrap( ) , body) ;
790+ assert ! (
791+ attempts >= 2 ,
792+ "a body-level failure must be retried, but get_bytes only made {attempts} connection(s)"
793+ ) ;
794+ }
795+
742796 #[ tokio:: test]
743797 #[ ignore] // Flaky on musl/Alpine — temp file race condition
744798 async fn test_verify_file_hash_sha1 ( ) {
0 commit comments