From d370446c1b4f37055f359546e57f88e465f9746f Mon Sep 17 00:00:00 2001 From: Kailan Blanks Date: Fri, 24 Apr 2026 16:21:37 +0100 Subject: [PATCH] Wrap backend connection errors with backend information --- CHANGELOG.md | 1 + src/component/compute/error.rs | 11 +++++++++++ src/component/http_req.rs | 8 +++++--- src/error.rs | 28 ++++++++++++++++++++++++++++ src/upstream.rs | 14 +++++++++++--- src/wiggle_abi/req_impl.rs | 20 +++++++++++++++----- 6 files changed, 71 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b92686..983f217f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add `fake_valid_fastly_keys` config parameter to allow testing `fastly_key_is_valid` hostcall with fake valid keys. ([#599](https://github.com/fastly/Viceroy/pull/599)) - Add `health` config parameter for backends to mock backend health status in testing. ([#605](https://github.com/fastly/Viceroy/pulls/606)) - Use `cargo clippy` to lint code in CI. ([#603](https://github.com/fastly/Viceroy/pull/603)) +- Provide extra context in error messages for backend connection failures ([#613](https://github.com/fastly/Viceroy/pull/613)) ## 0.16.5 (2026-03-23) diff --git a/src/component/compute/error.rs b/src/component/compute/error.rs index 07b8941f..fdb6ef38 100644 --- a/src/component/compute/error.rs +++ b/src/component/compute/error.rs @@ -210,6 +210,17 @@ impl From for types::Error { Error::HyperError(e) if e.is_user() => types::Error::HttpUser, Error::HyperError(e) if e.is_incomplete_message() => types::Error::HttpIncomplete, Error::HyperError(_) => types::Error::GenericError, + // BackendConnectionError wraps hyper::Error with context + Error::BackendConnectionError { source, .. } if source.is_parse() => { + types::Error::HttpInvalid + } + Error::BackendConnectionError { source, .. } if source.is_user() => { + types::Error::HttpUser + } + Error::BackendConnectionError { source, .. } if source.is_incomplete_message() => { + types::Error::HttpIncomplete + } + Error::BackendConnectionError { .. } => types::Error::GenericError, // Destructuring a GuestError is recursive, so we use a helper function: Error::GuestError(e) => e.into(), // We delegate to some error types' own implementation of `to_fastly_status`. diff --git a/src/component/http_req.rs b/src/component/http_req.rs index bc31e983..910fc1a1 100644 --- a/src/component/http_req.rs +++ b/src/component/http_req.rs @@ -64,7 +64,7 @@ pub(crate) async fn send( // synchronously send the request // This initial implementation ignores the error detail field let tls_config = session.tls_config(); - let resp = upstream::send_request(req, backend, tls_config) + let resp = upstream::send_request(req, backend, backend_name, tls_config) .await .map_err(Into::into) .map_err(types::Error::with_empty_detail)?; @@ -98,7 +98,8 @@ pub(crate) async fn send_async( // asynchronously send the request let tls_config = session.tls_config(); - let task = PeekableTask::spawn(upstream::send_request(req, backend, tls_config)).await; + let task = PeekableTask::spawn(upstream::send_request(req, backend, backend_name, tls_config)) + .await; // return a handle to the pending request Ok(session.insert_pending_request(task).into()) @@ -138,7 +139,8 @@ pub(crate) async fn send_async_streaming( // asynchronously send the request let tls_config = session.tls_config(); - let task = PeekableTask::spawn(upstream::send_request(req, backend, tls_config)).await; + let task = PeekableTask::spawn(upstream::send_request(req, backend, backend_name, tls_config)) + .await; // return a handle to the pending request Ok(session.insert_pending_request(task).into()) diff --git a/src/error.rs b/src/error.rs index 1f20ef50..8dda5a25 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,14 @@ pub enum Error { #[error(transparent)] HyperError(#[from] hyper::Error), + #[error("Backend connection error for '{backend_name}' ({uri}): {source}")] + BackendConnectionError { + backend_name: String, + uri: String, + #[source] + source: hyper::Error, + }, + #[error(transparent)] Infallible(#[from] std::convert::Infallible), @@ -190,6 +198,26 @@ impl Error { FastlyStatus::Httpincomplete } Error::HyperError(_) => FastlyStatus::Error, + // BackendConnectionError contains detailed context but maps to same status as HyperError + Error::BackendConnectionError { source, .. } if source.is_parse() => { + FastlyStatus::Httpinvalid + } + Error::BackendConnectionError { source, .. } if source.is_user() => { + FastlyStatus::Httpuser + } + Error::BackendConnectionError { source, .. } if source.is_incomplete_message() => { + FastlyStatus::Httpincomplete + } + Error::BackendConnectionError { source, .. } + if source + .source() + .and_then(|e| e.downcast_ref::()) + .map(|ioe| ioe.kind()) + == Some(io::ErrorKind::UnexpectedEof) => + { + FastlyStatus::Httpincomplete + } + Error::BackendConnectionError { .. } => FastlyStatus::Error, // Destructuring a GuestError is recursive, so we use a helper function: Error::GuestError(e) => Self::guest_error_fastly_status(e), // We delegate to some error types' own implementation of `to_fastly_status`. diff --git a/src/upstream.rs b/src/upstream.rs index 2bbb02c9..ca18b80f 100644 --- a/src/upstream.rs +++ b/src/upstream.rs @@ -288,6 +288,7 @@ fn canonical_uri(original_uri: &Uri, canonical_host: &str, backend: &Backend) -> pub fn send_request( mut req: Request, backend: &Arc, + backend_name: &str, tls_config: &TlsConfig, ) -> impl Future, Error>> + use<> { let connector = BackendConnector::new(backend.clone(), tls_config.clone()); @@ -342,6 +343,8 @@ pub fn send_request( *req.uri_mut() = uri; let h2only = backend.grpc; + let backend_name = backend_name.to_string(); + let backend_uri = backend.uri.to_string(); async move { let mut builder = Client::builder(); @@ -361,9 +364,14 @@ pub fn send_request( .build(connector) .request(req) .await - .map_err(|e| { - eprintln!("Error: {:?}", e); - e + .map_err(|source| { + let err = Error::BackendConnectionError { + backend_name: backend_name.clone(), + uri: backend_uri.clone(), + source, + }; + tracing::error!("{}", err); + err })?; if let Some(md) = basic_response.extensions_mut().get_mut::() { diff --git a/src/wiggle_abi/req_impl.rs b/src/wiggle_abi/req_impl.rs index 7ecc4287..1d90ecc4 100644 --- a/src/wiggle_abi/req_impl.rs +++ b/src/wiggle_abi/req_impl.rs @@ -834,7 +834,7 @@ impl FastlyHttpReq for Session { .ok_or_else(|| Error::UnknownBackend(backend_name.to_owned()))?; // synchronously send the request - let resp = upstream::send_request(req, backend, self.tls_config()).await?; + let resp = upstream::send_request(req, backend, backend_name, self.tls_config()).await?; Ok(self.insert_response(resp)) } @@ -884,8 +884,13 @@ impl FastlyHttpReq for Session { .ok_or_else(|| Error::UnknownBackend(backend_name.to_owned()))?; // asynchronously send the request - let task = - PeekableTask::spawn(upstream::send_request(req, backend, self.tls_config())).await; + let task = PeekableTask::spawn(upstream::send_request( + req, + backend, + backend_name, + self.tls_config(), + )) + .await; // return a handle to the pending task Ok(self.insert_pending_request(task)) @@ -929,8 +934,13 @@ impl FastlyHttpReq for Session { .ok_or_else(|| Error::UnknownBackend(backend_name.to_owned()))?; // asynchronously send the request - let task = - PeekableTask::spawn(upstream::send_request(req, backend, self.tls_config())).await; + let task = PeekableTask::spawn(upstream::send_request( + req, + backend, + backend_name, + self.tls_config(), + )) + .await; // return a handle to the pending task Ok(self.insert_pending_request(task))