Skip to content

Commit 9cda4c3

Browse files
authored
Merge branch 'main' into fix/bundle-tsdown-exe-css
2 parents 15365b2 + 1bbf3f1 commit 9cda4c3

1 file changed

Lines changed: 69 additions & 15 deletions

File tree

crates/vite_install/src/request.rs

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nContent-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

Comments
 (0)