Skip to content

Commit 5dd1f99

Browse files
committed
feat(http): surface full error source chain for download failures
The Linux sfw CI failure showed: Failed to download from .../SHASUMS256.txt: error sending request for url (https://.../SHASUMS256.txt) The user-visible message stopped at reqwest's top-level Display and dropped the actual cause (e.g. "invalid peer certificate: UnknownIssuer" inside the rustls error chain). That hides why the request failed — looks like a network blip when it's actually a TLS trust problem. Add `vite_shared::format_error_chain` that walks `Error::source()` recursively and joins the messages with `: `. Use it in all four `Error::DownloadFailed { reason }` map_err sites in vite_js_runtime/src/download.rs. Now the same failure renders as: Failed to download from .../SHASUMS256.txt: error sending request for url (...): client error (Connect): invalid peer certificate: UnknownIssuer making the root cause grep-able from the CI log.
1 parent 0bb4f98 commit 5dd1f99

3 files changed

Lines changed: 82 additions & 7 deletions

File tree

crates/vite_js_runtime/src/download.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ pub async fn download_file(
4949
.with_max_times(3),
5050
)
5151
.await
52-
.map_err(|e| Error::DownloadFailed { url: url.into(), reason: vite_str::format!("{e}") })?;
52+
.map_err(|e| Error::DownloadFailed {
53+
url: url.into(),
54+
reason: vite_shared::format_error_chain(&e).into(),
55+
})?;
5356

5457
// Get Content-Length for progress bar
5558
let total_size = response.content_length();
@@ -125,7 +128,10 @@ pub async fn download_text(url: &str) -> Result<String, Error> {
125128
.with_max_times(3),
126129
)
127130
.await
128-
.map_err(|e| Error::DownloadFailed { url: url.into(), reason: vite_str::format!("{e}") })?;
131+
.map_err(|e| Error::DownloadFailed {
132+
url: url.into(),
133+
reason: vite_shared::format_error_chain(&e).into(),
134+
})?;
129135

130136
Ok(content)
131137
}
@@ -158,7 +164,10 @@ pub async fn fetch_with_cache_headers(
158164
.with_max_times(3),
159165
)
160166
.await
161-
.map_err(|e| Error::DownloadFailed { url: url.into(), reason: vite_str::format!("{e}") })?;
167+
.map_err(|e| Error::DownloadFailed {
168+
url: url.into(),
169+
reason: vite_shared::format_error_chain(&e).into(),
170+
})?;
162171

163172
// Check for 304 Not Modified
164173
if response.status() == reqwest::StatusCode::NOT_MODIFIED {
@@ -181,10 +190,10 @@ pub async fn fetch_with_cache_headers(
181190
.and_then(|v| v.to_str().ok())
182191
.and_then(parse_max_age);
183192

184-
let body = response
185-
.text()
186-
.await
187-
.map_err(|e| Error::DownloadFailed { url: url.into(), reason: vite_str::format!("{e}") })?;
193+
let body = response.text().await.map_err(|e| Error::DownloadFailed {
194+
url: url.into(),
195+
reason: vite_shared::format_error_chain(&e).into(),
196+
})?;
188197

189198
Ok(CachedFetchResponse { body: Some(body), etag, max_age, not_modified: false })
190199
}

crates/vite_shared/src/error.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Error-formatting helpers.
2+
3+
use std::error::Error;
4+
5+
/// Format an error and its full `source()` chain as `top: cause: deeper-cause`.
6+
///
7+
/// Use this when stringifying an error into a field of a higher-level error
8+
/// type — otherwise the Display impl of types like `reqwest::Error` only shows
9+
/// the top-level message, hiding the actual cause (TLS handshake failure,
10+
/// connection refused, etc.).
11+
#[must_use]
12+
pub fn format_error_chain(err: &(dyn Error + 'static)) -> String {
13+
let mut out = err.to_string();
14+
let mut current = err.source();
15+
while let Some(source) = current {
16+
out.push_str(": ");
17+
out.push_str(&source.to_string());
18+
current = source.source();
19+
}
20+
out
21+
}
22+
23+
#[cfg(test)]
24+
mod tests {
25+
use std::{error::Error as StdError, fmt};
26+
27+
use super::*;
28+
29+
#[derive(Debug)]
30+
struct Layer {
31+
msg: &'static str,
32+
cause: Option<Box<Layer>>,
33+
}
34+
35+
impl fmt::Display for Layer {
36+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
f.write_str(self.msg)
38+
}
39+
}
40+
41+
impl StdError for Layer {
42+
fn source(&self) -> Option<&(dyn StdError + 'static)> {
43+
self.cause.as_deref().map(|c| c as &(dyn StdError + 'static))
44+
}
45+
}
46+
47+
#[test]
48+
fn single_error_no_chain() {
49+
let e = Layer { msg: "top", cause: None };
50+
assert_eq!(format_error_chain(&e), "top");
51+
}
52+
53+
#[test]
54+
fn walks_full_chain() {
55+
let e = Layer {
56+
msg: "send request",
57+
cause: Some(Box::new(Layer {
58+
msg: "tls handshake",
59+
cause: Some(Box::new(Layer { msg: "UnknownIssuer", cause: None })),
60+
})),
61+
};
62+
assert_eq!(format_error_chain(&e), "send request: tls handshake: UnknownIssuer");
63+
}
64+
}

crates/vite_shared/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
mod env_config;
1111
pub mod env_vars;
12+
mod error;
1213
pub mod header;
1314
mod home;
1415
mod http;
@@ -20,6 +21,7 @@ mod tls;
2021
mod tracing;
2122

2223
pub use env_config::{EnvConfig, TestEnvGuard};
24+
pub use error::format_error_chain;
2325
pub use home::get_vp_home;
2426
pub use http::shared_http_client;
2527
pub use package_json::{DevEngines, Engines, PackageJson, RuntimeEngine, RuntimeEngineConfig};

0 commit comments

Comments
 (0)