From 04dd5d162e3b428661f867be7c85679cb3f5d4cd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 10 May 2026 22:14:20 +0800 Subject: [PATCH] fix(update): better timeout Signed-off-by: fufesou --- src/hbbs_http/downloader.rs | 48 +++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/hbbs_http/downloader.rs b/src/hbbs_http/downloader.rs index ac4bcd5eb..7240a1535 100644 --- a/src/hbbs_http/downloader.rs +++ b/src/hbbs_http/downloader.rs @@ -18,6 +18,9 @@ lazy_static! { static ref DOWNLOADERS: Mutex> = Default::default(); } +const DOWNLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); +const DOWNLOAD_READ_TIMEOUT: Duration = Duration::from_secs(60); + /// This struct is used to return the download data to the caller. /// The caller should check if the file is downloaded successfully and remove the job from the map. /// If the file is not downloaded successfully, the `data` field will be empty. @@ -175,16 +178,24 @@ async fn do_download( auto_del_dur: Option, mut rx_cancel: UnboundedReceiver<()>, ) -> ResultType { - let client = create_http_client_async_with_url(&url, true).await; + let client = match tokio::time::timeout( + DOWNLOAD_REQUEST_TIMEOUT, + create_http_client_async_with_url(&url, true), + ) + .await + { + Ok(client) => client, + Err(_) => bail!("Timed out while creating download HTTP client"), + }; let mut is_all_downloaded = false; tokio::select! { _ = rx_cancel.recv() => { return Ok(is_all_downloaded); } - head_resp = client.head(&url).send() => { + head_resp = tokio::time::timeout(DOWNLOAD_REQUEST_TIMEOUT, client.head(&url).send()) => { match head_resp { - Ok(resp) => { + Ok(Ok(resp)) => { if resp.status().is_success() { let total_size = resp .headers() @@ -201,9 +212,10 @@ async fn do_download( bail!("Failed to get content length: {}", resp.status()); } } - Err(e) => { + Ok(Err(e)) => { return Err(e.into()); } + Err(_) => bail!("Timed out while getting download file size"), } } } @@ -213,8 +225,12 @@ async fn do_download( _ = rx_cancel.recv() => { return Ok(is_all_downloaded); } - resp = client.get(url).send() => { - response = resp?; + resp = tokio::time::timeout(DOWNLOAD_REQUEST_TIMEOUT, client.get(&url).send()) => { + match resp { + Ok(Ok(resp)) => response = resp, + Ok(Err(e)) => return Err(e.into()), + Err(_) => bail!("Timed out while starting download"), + } } } @@ -228,9 +244,9 @@ async fn do_download( _ = rx_cancel.recv() => { break; } - chunk = response.chunk() => { + chunk = tokio::time::timeout(DOWNLOAD_READ_TIMEOUT, response.chunk()) => { match chunk { - Ok(Some(chunk)) => { + Ok(Ok(Some(chunk))) => { match dest { Some(ref mut f) => { f.write_all(&chunk).await?; @@ -247,14 +263,15 @@ async fn do_download( } } } - Ok(None) => { + Ok(Ok(None)) => { is_all_downloaded = true; break; }, - Err(e) => { + Ok(Err(e)) => { log::error!("Download {} failed: {}", id, e); return Err(e.into()); } + Err(_) => bail!("Timed out while reading download data"), } } } @@ -315,3 +332,14 @@ pub fn cancel(id: &str) { pub fn remove(id: &str) { let _ = DOWNLOADERS.lock().unwrap().remove(id); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn download_timeouts_are_bounded() { + assert_eq!(DOWNLOAD_REQUEST_TIMEOUT, Duration::from_secs(30)); + assert_eq!(DOWNLOAD_READ_TIMEOUT, Duration::from_secs(60)); + } +}