Skip to content
Merged
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
32 changes: 32 additions & 0 deletions crates/aisix-gateway/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,38 @@ impl BridgeError {
}
}

/// Whether the request actually left for the upstream before this
/// error was raised.
///
/// Gates the `aisix_deployment_*` families, which read as **upstream
/// health** for one deployment target. The three config/credential
/// variants are raised while the request is still being assembled — an
/// empty or unusable `api_key`, a missing `model_name`/`api_base`, a
/// body that would not serialize, a `split_system` shape the provider
/// cannot express — so no provider was ever contacted, and counting
/// them against a deployment reports our own misconfiguration as
/// provider degradation.
///
/// `Timeout` and `Transport` stay `true` on purpose: a connect timeout
/// or a refused connection means we did try to reach the upstream, and
/// "unreachable" is exactly the kind of health this family exists to
/// show. Kept exhaustive (like [`http_status`](Self::http_status) and
/// `routing_error_class`) so a new variant has to declare which side of
/// the network boundary it sits on instead of inheriting a default.
pub fn reached_upstream(&self) -> bool {
match self {
BridgeError::Timeout { .. }
| BridgeError::UpstreamStatus { .. }
| BridgeError::UpstreamDecode(_)
| BridgeError::UpstreamInBand { .. }
| BridgeError::Transport(_)
Comment thread
jarvis9443 marked this conversation as resolved.
| BridgeError::StreamAborted => true,
BridgeError::Config(_)
| BridgeError::InvalidUpstreamConfig(_)
| BridgeError::InvalidUpstreamCredentials(_) => false,
}
}

/// Stable error-type token for the error envelope's `type` field.
pub fn error_type(&self) -> &'static str {
match self {
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ pub use upstream_headers::{
RESERVED_UPSTREAM_HEADERS,
};
pub use upstream_http::{
client_builder, error_with_causes, transport_error_message, UpstreamHttpConfig,
client_builder, error_with_causes, send_error, transport_error_message, UpstreamHttpConfig,
};
pub use upstream_tls::TlsSettings;
59 changes: 59 additions & 0 deletions crates/aisix-gateway/src/upstream_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use std::sync::OnceLock;
use std::time::Duration;

use crate::bridge::BridgeError;
use crate::upstream_tls::TlsSettings;

/// Suffixes marking a query parameter whose value is a credential and must
Expand Down Expand Up @@ -194,6 +195,33 @@ pub fn transport_error_message(err: &reqwest::Error) -> String {
msg
}

/// Classify a `reqwest` **send** failure into its [`BridgeError`].
///
/// reqwest reports a *builder* error when the request could not even be
/// constructed. In practice that is an `api_base` that does not parse as a
/// URL: [`crate::url_cache::EndpointUrl::Unparsed`] deliberately hands the
/// raw string to the request builder so the message stays exactly what it
/// always was, and the parse failure then surfaces here at `send()` with
/// `is_builder()` set and no URL attached.
///
/// Nothing was sent, so this is customer-fixable upstream config — the same
/// class as a *missing* `api_base`, which already maps to
/// [`BridgeError::InvalidUpstreamConfig`] — rather than a transport failure.
/// Calling it `Transport` would report a 502 for an operator's typo, retry
/// a URL that can never parse, and (via
/// [`BridgeError::reached_upstream`]) count it against the target's
/// `aisix_deployment_*` health even though no provider was contacted.
///
/// Use at `send()` sites only. A failure reading an already-open response
/// body or stream is never a builder error and stays [`BridgeError::Transport`].
pub fn send_error(err: reqwest::Error) -> BridgeError {
if err.is_builder() {
BridgeError::InvalidUpstreamConfig(transport_error_message(&err))
} else {
BridgeError::Transport(transport_error_message(&err))
}
}

/// Same as [`transport_error_message`] for error types that aren't
/// `reqwest::Error` (websocket handshakes, SDK dispatch errors) — no URL
/// is available to redact, so only the cause chain is appended.
Expand Down Expand Up @@ -635,4 +663,35 @@ mod tests {
"causes must add information"
);
}

/// The distinction `send_error` exists to make. `EndpointUrl::Unparsed`
/// hands a malformed `api_base` to the request builder verbatim, and
/// reqwest reports the parse failure only here, at `send()`, as a
/// builder error with no URL attached. Classifying it as `Transport`
/// would 502 an operator's typo, retry a URL that can never parse, and
/// count it against the target's upstream health.
#[tokio::test]
async fn builder_errors_are_upstream_config_not_transport() {
let client = reqwest::Client::new();
let builder_err = crate::url_cache::EndpointUrl::Unparsed("ht tp://not a url".to_string())
.post_on(&client)
.send()
.await
.expect_err("a malformed api_base cannot produce a response");
assert!(builder_err.is_builder());
assert!(matches!(
send_error(builder_err),
BridgeError::InvalidUpstreamConfig(_)
));

// A real connection attempt to a closed port stays transport: we
// did try to reach the provider, and that is upstream health.
let io_err = client
.post("http://127.0.0.1:1/v1/chat/completions")
.send()
.await
.expect_err("nothing listens on port 1");
assert!(!io_err.is_builder());
assert!(matches!(send_error(io_err), BridgeError::Transport(_)));
}
}
4 changes: 2 additions & 2 deletions crates/aisix-provider-anthropic/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ impl Bridge for AnthropicBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -355,7 +355,7 @@ impl Bridge for AnthropicBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down
4 changes: 2 additions & 2 deletions crates/aisix-provider-azure-openai/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ impl Bridge for AzureOpenAiBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -780,7 +780,7 @@ impl Bridge for AzureOpenAiBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down
10 changes: 5 additions & 5 deletions crates/aisix-provider-openai/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ impl Bridge for OpenAiBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -470,7 +470,7 @@ impl Bridge for OpenAiBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -529,7 +529,7 @@ impl Bridge for OpenAiBridge {
.json(&outbound)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -586,7 +586,7 @@ impl Bridge for OpenAiBridge {
.json(&outbound)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -634,7 +634,7 @@ impl Bridge for OpenAiBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down
18 changes: 9 additions & 9 deletions crates/aisix-provider-vertex/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ impl Bridge for VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;
let status = resp.status();
if !status.is_success() {
return Err(map_http_error(status, resp).await);
Expand Down Expand Up @@ -912,7 +912,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -1017,7 +1017,7 @@ impl VertexBridge {
.json(&body_value)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;

let status = resp.status();
if !status.is_success() {
Expand Down Expand Up @@ -1113,7 +1113,7 @@ impl VertexBridge {
.json(&body_value)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down Expand Up @@ -1233,7 +1233,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;
let status = resp.status();
if !status.is_success() {
return Err(map_http_error(status, resp).await);
Expand Down Expand Up @@ -1294,7 +1294,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down Expand Up @@ -1437,7 +1437,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))?;
.map_err(aisix_gateway::send_error)?;
let status = resp.status();
if !status.is_success() {
return Err(map_http_error(status, resp).await);
Expand Down Expand Up @@ -1515,7 +1515,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down Expand Up @@ -1635,7 +1635,7 @@ impl VertexBridge {
.json(&body)
.send()
.await
.map_err(|e| BridgeError::Transport(aisix_gateway::transport_error_message(&e)))
.map_err(aisix_gateway::send_error)
})
.await?;

Expand Down
Loading