Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions src/component/compute/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ impl From<error::Error> 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`.
Expand Down
8 changes: 5 additions & 3 deletions src/component/http_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
28 changes: 28 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down Expand Up @@ -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::<io::Error>())
.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`.
Expand Down
14 changes: 11 additions & 3 deletions src/upstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ fn canonical_uri(original_uri: &Uri, canonical_host: &str, backend: &Backend) ->
pub fn send_request(
mut req: Request<Body>,
backend: &Arc<Backend>,
backend_name: &str,
tls_config: &TlsConfig,
) -> impl Future<Output = Result<Response<Body>, Error>> + use<> {
let connector = BackendConnector::new(backend.clone(), tls_config.clone());
Expand Down Expand Up @@ -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();

Expand All @@ -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::<ConnMetadata>() {
Expand Down
20 changes: 15 additions & 5 deletions src/wiggle_abi/req_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
Loading