fix(logging): sanitize transport error display

This commit is contained in:
elky
2026-09-04 19:49:01 +08:00
parent b72b6ab137
commit 36e9d21e3f
@@ -904,19 +904,19 @@ pub(crate) enum ExecutionRuntimeTransportError {
BodyDecode(base64::DecodeError), BodyDecode(base64::DecodeError),
#[error("request body exceeds {limit_bytes} decoded bytes")] #[error("request body exceeds {limit_bytes} decoded bytes")]
BodyTooLarge { limit_bytes: usize }, BodyTooLarge { limit_bytes: usize },
#[error("request content-encoding is not supported: {0}")] #[error("request content-encoding is not supported: {}", sanitize_error_detail(.0))]
UnsupportedContentEncoding(String), UnsupportedContentEncoding(String),
#[error("proxy execution is not supported")] #[error("proxy execution is not supported")]
ProxyUnsupported, ProxyUnsupported,
#[error("invalid method: {0}")] #[error("invalid method: {}", sanitize_error_detail(&.0.to_string()))]
InvalidMethod(#[from] http::method::InvalidMethod), InvalidMethod(#[from] http::method::InvalidMethod),
#[error("invalid upstream header name: {0}")] #[error("invalid upstream header name: {}", sanitize_error_detail(.0))]
InvalidHeaderName(String), InvalidHeaderName(String),
#[error("invalid upstream header value for {0}")] #[error("invalid upstream header value for {}", sanitize_error_detail(.0))]
InvalidHeaderValue(String), InvalidHeaderValue(String),
#[error("invalid proxy configuration")] #[error("invalid proxy configuration")]
InvalidProxy(#[source] reqwest::Error), InvalidProxy(#[source] reqwest::Error),
#[error("unsupported transport profile backend: {0}")] #[error("unsupported transport profile backend: {}", sanitize_error_detail(.0))]
UnsupportedTransportProfile(String), UnsupportedTransportProfile(String),
#[error("failed to encode request body")] #[error("failed to encode request body")]
BodyEncode(#[source] serde_json::Error), BodyEncode(#[source] serde_json::Error),
@@ -924,20 +924,24 @@ pub(crate) enum ExecutionRuntimeTransportError {
ClientBuild(#[source] reqwest::Error), ClientBuild(#[source] reqwest::Error),
#[error("failed to build browser impersonation HTTP client")] #[error("failed to build browser impersonation HTTP client")]
BrowserClientBuild(#[source] wreq::Error), BrowserClientBuild(#[source] wreq::Error),
#[error("browser impersonation response body failed: {0}")] #[error("browser impersonation response body failed: {}", sanitize_error_detail(.0))]
BrowserBody(String), BrowserBody(String),
#[error("{message}")] #[error("{}", sanitize_error_detail(message))]
UpstreamHttpStatus { status_code: u16, message: String }, UpstreamHttpStatus { status_code: u16, message: String },
#[error("failed to execute upstream request: {0}")] #[error("failed to execute upstream request: {}", sanitize_error_detail(.0))]
UpstreamRequest(String), UpstreamRequest(String),
#[error("upstream response {phase} body exceeds {limit_bytes} bytes")] #[error("upstream response {phase} body exceeds {limit_bytes} bytes")]
UpstreamResponseTooLarge { UpstreamResponseTooLarge {
phase: UpstreamResponseBodyPhase, phase: UpstreamResponseBodyPhase,
limit_bytes: usize, limit_bytes: usize,
}, },
#[error("failed to decode upstream response body with content-encoding {encoding}: {message}")] #[error(
"failed to decode upstream response body with content-encoding {}: {}",
sanitize_error_detail(encoding),
sanitize_error_detail(message)
)]
UpstreamResponseDecode { encoding: String, message: String }, UpstreamResponseDecode { encoding: String, message: String },
#[error("hub relay request failed: {0}")] #[error("hub relay request failed: {}", sanitize_error_detail(.0))]
RelayError(String), RelayError(String),
#[error("upstream response is not valid JSON: {0}")] #[error("upstream response is not valid JSON: {0}")]
InvalidJson(serde_json::Error), InvalidJson(serde_json::Error),
@@ -946,10 +950,10 @@ pub(crate) enum ExecutionRuntimeTransportError {
// `reqwest::Error` and `wreq::Error` retain the URL associated with a failed // `reqwest::Error` and `wreq::Error` retain the URL associated with a failed
// request. Their derived `Debug` implementations therefore may include // request. Their derived `Debug` implementations therefore may include
// proxy credentials or query-string tokens. This error is logged with // proxy credentials or query-string tokens. This error is logged with
// structured `?error` fields in a few execution paths, so keep `Display` // structured `?error` fields in a few execution paths, so both `Debug` and
// (which is intentionally stable for callers) separate from a safe diagnostic // `Display` must be safe if a caller accidentally crosses that boundary.
// representation. Dynamic details are passed through the same URL-aware, // Dynamic details are passed through the same URL-aware, bounded sanitizer
// bounded sanitizer used by the upstream request formatters. // used by the upstream request formatters.
impl std::fmt::Debug for ExecutionRuntimeTransportError { impl std::fmt::Debug for ExecutionRuntimeTransportError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -5828,6 +5832,16 @@ mod tests {
assert!(!upstream_message.contains("fragment-secret")); assert!(!upstream_message.contains("fragment-secret"));
assert!(!upstream_message.contains("127.0.0.1")); assert!(!upstream_message.contains("127.0.0.1"));
assert!(upstream_message.contains("redacted.invalid")); assert!(upstream_message.contains("redacted.invalid"));
// Display is used by a few legacy error/logging boundaries. Keep it
// safe as well, so a missed `?error`/`safe_transport_error_message`
// conversion cannot reintroduce URL credential leakage.
let upstream_display = format!("{upstream}");
assert!(!upstream_display.contains("upstream-user"));
assert!(!upstream_display.contains("upstream-password"));
assert!(!upstream_display.contains("query-secret"));
assert!(!upstream_display.contains("fragment-secret"));
assert!(!upstream_display.contains("127.0.0.1"));
assert!(upstream_display.contains("redacted.invalid"));
let status = ExecutionRuntimeTransportError::UpstreamHttpStatus { let status = ExecutionRuntimeTransportError::UpstreamHttpStatus {
status_code: 502, status_code: 502,
@@ -5837,6 +5851,33 @@ mod tests {
assert!(!status_debug.contains("upstream-password")); assert!(!status_debug.contains("upstream-password"));
assert!(!status_debug.contains("query-secret")); assert!(!status_debug.contains("query-secret"));
assert!(!status_debug.contains("127.0.0.1")); assert!(!status_debug.contains("127.0.0.1"));
let status_display = format!("{status}");
assert!(!status_display.contains("upstream-user"));
assert!(!status_display.contains("upstream-password"));
assert!(!status_display.contains("query-secret"));
assert!(!status_display.contains("fragment-secret"));
assert!(!status_display.contains("127.0.0.1"));
let decode = ExecutionRuntimeTransportError::UpstreamResponseDecode {
encoding: "gzip".to_string(),
message: format!("decode failed for {secret_url}"),
};
let decode_display = format!("{decode}");
assert!(!decode_display.contains("upstream-user"));
assert!(!decode_display.contains("upstream-password"));
assert!(!decode_display.contains("query-secret"));
assert!(!decode_display.contains("fragment-secret"));
assert!(!decode_display.contains("127.0.0.1"));
let relay = ExecutionRuntimeTransportError::RelayError(format!(
"relay failed while contacting {secret_url}"
));
let relay_display = format!("{relay}");
assert!(!relay_display.contains("upstream-user"));
assert!(!relay_display.contains("upstream-password"));
assert!(!relay_display.contains("query-secret"));
assert!(!relay_display.contains("fragment-secret"));
assert!(!relay_display.contains("127.0.0.1"));
let source = reqwest::Proxy::all("http://[") let source = reqwest::Proxy::all("http://[")
.expect_err("malformed proxy should produce a reqwest error") .expect_err("malformed proxy should produce a reqwest error")