diff --git a/docs/tst-desk-backlog.md b/docs/tst-desk-backlog.md index 360c04aa..dc97b2e2 100644 --- a/docs/tst-desk-backlog.md +++ b/docs/tst-desk-backlog.md @@ -13041,6 +13041,23 @@ step 1; elsewhere the binary comes from `binary` or `PATH`. --- +### TD-5229 — A failed update check says why +**Size:** 1 · **Depends on:** TD-4837, TD-5204 +**Status:** In review 2026-11-03. + +Settings → About → Check for updates showed only "release fetch failed: +error sending request for url (https://api.github.com/...)". That is +reqwest's top line; the reason (no route, a refused connection, a DNS or +certificate failure) is in the error's source chain, which this path dropped. +On the Mac where it was reported, the same request with the updater's own +client settings returned 200 a few hours later (real certificate, no proxy), +so the failure was the network at that moment, and the message could not say +so. + +**Acceptance criteria:** +- [x] A check that gets no answer says in words what failed (timed out, could not connect, or the request failed), asks to check the network and try again, and keeps the full cause chain as details +- [x] Reading the release list, the checksum file, or a download chunk keeps the cause chain too +- [x] A unit test makes a real refused connection on loopback and finds the OS reason in the message; it fails against the old message ### TD-5226 — The ring marks the display the agent is acting on, and only in full control **Size:** 3 · **Depends on:** TD-3402, TD-3407, TD-5223 **Status:** In review 2026-11-03. diff --git a/shell/src/updater/fetch.rs b/shell/src/updater/fetch.rs index ce3c8df9..20d86fe9 100644 --- a/shell/src/updater/fetch.rs +++ b/shell/src/updater/fetch.rs @@ -37,8 +37,10 @@ pub fn client() -> Result { .map_err(|e| format!("client build failed: {e}")) } -/// reqwest's Display stops at "error following redirect for url (...)"; -/// the reason (which host was refused) is in the source chain. +/// reqwest's Display stops at "error following redirect for url (...)" or +/// "error sending request for url (...)"; the reason (which host was +/// refused, a refused connection, a DNS or certificate failure) is in the +/// source chain. fn with_causes(e: &reqwest::Error) -> String { let mut text = e.to_string(); let mut source = std::error::Error::source(e); @@ -53,6 +55,24 @@ fn with_causes(e: &reqwest::Error) -> String { text } +/// The update check could not get an answer from GitHub (TD-5229). Says +/// what kind of failure it was in words, then the full cause chain, so the +/// message alone tells a dropped network from a refused or intercepted one. +pub(crate) fn unreachable_message(e: &reqwest::Error) -> String { + let what = if e.is_timeout() { + "GitHub did not answer in time" + } else if e.is_connect() { + "Could not connect to GitHub" + } else { + "The request to GitHub failed" + }; + format!( + "{what}, so the update check stopped. Check the network connection and try again. \ + Details: {}", + with_causes(e) + ) +} + pub async fn fetch_releases(http: &reqwest::Client) -> Result, String> { let url = releases_url(); if !releases_api_allowed(&url) { @@ -63,7 +83,7 @@ pub async fn fetch_releases(http: &reqwest::Client) -> Result, Stri .header("Accept", "application/vnd.github+json") .send() .await - .map_err(|e| format!("release fetch failed: {e}"))?; + .map_err(|e| unreachable_message(&e))?; if response.status() == reqwest::StatusCode::FORBIDDEN || response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { @@ -75,7 +95,7 @@ pub async fn fetch_releases(http: &reqwest::Client) -> Result, Stri let body = response .text() .await - .map_err(|e| format!("release read failed: {e}"))?; + .map_err(|e| format!("release read failed: {}", with_causes(&e)))?; if body.len() > MAX_SUMS_BYTES * 5 { return Err("the release list was larger than expected".into()); } @@ -90,7 +110,7 @@ pub async fn download_text(http: &reqwest::Client, url: &str) -> Result MAX_SUMS_BYTES { return Err("SHA256SUMS.txt was larger than expected".into()); } @@ -129,7 +149,7 @@ pub async fn download_verified( let mut got: u64 = 0; let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { - let chunk = chunk.map_err(|e| format!("download failed: {e}"))?; + let chunk = chunk.map_err(|e| format!("download failed: {}", with_causes(&e)))?; got += chunk.len() as u64; if got > MAX_DOWNLOAD_BYTES { let _ = tokio::fs::remove_file(&partial).await; @@ -167,3 +187,25 @@ async fn send_download(http: &reqwest::Client, url: &str) -> Result