use http::{HeaderMap, Method};
use std::time::{Duration, SystemTime};
use crate::util::parse_retry_after;
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub(crate) fn summarize_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
let mut messages = Vec::new();
let mut current = Some(error);
while let Some(source) = current {
let message = source.to_string();
if messages.last() != Some(&message) {
messages.push(message);
}
current = source.source();
}
messages.join(": ")
}
pub(crate) fn transport_error(
kind: TransportErrorKind,
method: Method,
uri: String,
source: impl std::error::Error + Send + Sync + 'static,
) -> Error {
let source: BoxError = Box::new(source);
let message = summarize_error_chain(source.as_ref());
Error::Transport {
kind,
method,
uri,
message,
source,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum TransportErrorKind {
Dns,
Connect,
Tls,
Read,
Other,
}
impl std::fmt::Display for TransportErrorKind {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Self::Dns => "dns",
Self::Connect => "connect",
Self::Tls => "tls",
Self::Read => "read",
Self::Other => "other",
};
formatter.write_str(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum TimeoutPhase {
Transport,
ResponseBody,
}
impl std::fmt::Display for TimeoutPhase {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Self::Transport => "transport",
Self::ResponseBody => "response_body",
};
formatter.write_str(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
InvalidUri,
InvalidNoProxyRule,
InvalidProxyConfig,
InvalidAdaptiveConcurrencyPolicy,
SerializeJson,
SerializeQuery,
SerializeForm,
RequestBuild,
Transport,
Timeout,
DeadlineExceeded,
ReadBody,
WriteBody,
ResponseBodyTooLarge,
HttpStatus,
DeserializeJson,
DecodeText,
InvalidHeaderName,
InvalidHeaderValue,
DecodeContentEncoding,
ConcurrencyLimitClosed,
TlsBackendUnavailable,
TlsBackendInit,
TlsConfig,
RetryBudgetExhausted,
CircuitOpen,
MissingRedirectLocation,
InvalidRedirectLocation,
RedirectLimitExceeded,
RedirectBodyNotReplayable,
}
impl ErrorCode {
const ALL: &'static [Self] = &[
Self::InvalidUri,
Self::InvalidNoProxyRule,
Self::InvalidProxyConfig,
Self::InvalidAdaptiveConcurrencyPolicy,
Self::SerializeJson,
Self::SerializeQuery,
Self::SerializeForm,
Self::RequestBuild,
Self::Transport,
Self::Timeout,
Self::DeadlineExceeded,
Self::ReadBody,
Self::WriteBody,
Self::ResponseBodyTooLarge,
Self::HttpStatus,
Self::DeserializeJson,
Self::DecodeText,
Self::InvalidHeaderName,
Self::InvalidHeaderValue,
Self::DecodeContentEncoding,
Self::ConcurrencyLimitClosed,
Self::TlsBackendUnavailable,
Self::TlsBackendInit,
Self::TlsConfig,
Self::RetryBudgetExhausted,
Self::CircuitOpen,
Self::MissingRedirectLocation,
Self::InvalidRedirectLocation,
Self::RedirectLimitExceeded,
Self::RedirectBodyNotReplayable,
];
pub const fn all() -> &'static [Self] {
Self::ALL
}
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidUri => "invalid_uri",
Self::InvalidNoProxyRule => "invalid_no_proxy_rule",
Self::InvalidProxyConfig => "invalid_proxy_config",
Self::InvalidAdaptiveConcurrencyPolicy => "invalid_adaptive_concurrency_policy",
Self::SerializeJson => "serialize_json",
Self::SerializeQuery => "serialize_query",
Self::SerializeForm => "serialize_form",
Self::RequestBuild => "request_build",
Self::Transport => "transport",
Self::Timeout => "timeout",
Self::DeadlineExceeded => "deadline_exceeded",
Self::ReadBody => "read_body",
Self::WriteBody => "write_body",
Self::ResponseBodyTooLarge => "response_body_too_large",
Self::HttpStatus => "http_status",
Self::DeserializeJson => "deserialize_json",
Self::DecodeText => "decode_text",
Self::InvalidHeaderName => "invalid_header_name",
Self::InvalidHeaderValue => "invalid_header_value",
Self::DecodeContentEncoding => "decode_content_encoding",
Self::ConcurrencyLimitClosed => "concurrency_limit_closed",
Self::TlsBackendUnavailable => "tls_backend_unavailable",
Self::TlsBackendInit => "tls_backend_init",
Self::TlsConfig => "tls_config",
Self::RetryBudgetExhausted => "retry_budget_exhausted",
Self::CircuitOpen => "circuit_open",
Self::MissingRedirectLocation => "missing_redirect_location",
Self::InvalidRedirectLocation => "invalid_redirect_location",
Self::RedirectLimitExceeded => "redirect_limit_exceeded",
Self::RedirectBodyNotReplayable => "redirect_body_not_replayable",
}
}
}
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid request uri: {uri}")]
InvalidUri {
uri: String,
},
#[error("invalid no_proxy rule: {rule:?}")]
InvalidNoProxyRule {
rule: String,
},
#[error("invalid proxy configuration for {proxy_uri}: {message}")]
InvalidProxyConfig {
proxy_uri: String,
message: String,
},
#[error("proxy_authorization requires http_proxy to be configured")]
ProxyAuthorizationRequiresHttpProxy,
#[error(
"invalid adaptive concurrency policy (min={min_limit}, initial={initial_limit}, max={max_limit}): {message}"
)]
InvalidAdaptiveConcurrencyPolicy {
min_limit: usize,
initial_limit: usize,
max_limit: usize,
message: &'static str,
},
#[error("failed to serialize request json: {source}")]
SerializeJson {
#[source]
source: serde_json::Error,
},
#[error("failed to serialize request query: {source}")]
SerializeQuery {
#[source]
source: serde_urlencoded::ser::Error,
},
#[error("failed to serialize request form: {source}")]
SerializeForm {
#[source]
source: serde_urlencoded::ser::Error,
},
#[error("failed to build http request: {source}")]
RequestBuild {
#[source]
source: http::Error,
},
#[error("http transport error ({kind}) for {method} {uri}: {message}")]
Transport {
kind: TransportErrorKind,
method: Method,
uri: String,
message: String,
#[source]
source: BoxError,
},
#[error("http request timed out in {phase} after {timeout_ms}ms for {method} {uri}")]
Timeout {
phase: TimeoutPhase,
timeout_ms: u128,
method: Method,
uri: String,
},
#[error("http request deadline exceeded after {timeout_ms}ms for {method} {uri}")]
DeadlineExceeded {
timeout_ms: u128,
method: Method,
uri: String,
},
#[error("failed to read response body: {source}")]
ReadBody {
#[source]
source: BoxError,
},
#[error("failed to write response body for {method} {uri}: {source}")]
WriteBody {
method: Method,
uri: String,
#[source]
source: BoxError,
},
#[error(
"response body too large ({actual_bytes} bytes > {limit_bytes} bytes) for {method} {uri}"
)]
ResponseBodyTooLarge {
limit_bytes: usize,
actual_bytes: usize,
method: Method,
uri: String,
},
#[error("http status error {status} for {method} {uri}")]
HttpStatus {
status: u16,
method: Method,
uri: String,
headers: Box<HeaderMap>,
body: String,
},
#[error("failed to decode response json: {source}")]
DeserializeJson {
#[source]
source: serde_json::Error,
body: String,
},
#[error("failed to decode response text as utf-8: {source}")]
DecodeText {
#[source]
source: std::str::Utf8Error,
body: String,
},
#[error("invalid header name {name}: {source}")]
InvalidHeaderName {
name: String,
#[source]
source: http::header::InvalidHeaderName,
},
#[error("invalid header value for {name}: {source}")]
InvalidHeaderValue {
name: String,
#[source]
source: http::header::InvalidHeaderValue,
},
#[error("failed to decode response content-encoding {encoding} for {method} {uri}: {message}")]
DecodeContentEncoding {
encoding: String,
method: Method,
uri: String,
message: String,
},
#[error("request concurrency limiter is closed")]
ConcurrencyLimitClosed,
#[error("requested tls backend is not enabled in this build: {backend}")]
TlsBackendUnavailable {
backend: &'static str,
},
#[error("failed to initialize tls backend {backend}: {message}")]
TlsBackendInit {
backend: &'static str,
message: String,
},
#[error("invalid tls configuration for backend {backend}: {message}")]
TlsConfig {
backend: &'static str,
message: String,
},
#[error("retry budget exhausted for {method} {uri}")]
RetryBudgetExhausted {
method: Method,
uri: String,
},
#[error("circuit breaker is open for {method} {uri}; retry after {retry_after_ms}ms")]
CircuitOpen {
method: Method,
uri: String,
retry_after_ms: u128,
},
#[error("redirect response {status} missing location header for {method} {uri}")]
MissingRedirectLocation {
status: u16,
method: Method,
uri: String,
},
#[error("invalid redirect location {location} for {method} {uri}")]
InvalidRedirectLocation {
location: String,
method: Method,
uri: String,
},
#[error("redirect limit exceeded ({max_redirects}) for {method} {uri}")]
RedirectLimitExceeded {
max_redirects: usize,
method: Method,
uri: String,
},
#[error("cannot follow redirect for non-replayable request body: {method} {uri}")]
RedirectBodyNotReplayable {
method: Method,
uri: String,
},
}
impl std::fmt::Debug for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Error")
.field("code", &self.code())
.field("message", &self.to_string())
.finish()
}
}
impl Error {
pub const fn code(&self) -> ErrorCode {
match self {
Self::InvalidUri { .. } => ErrorCode::InvalidUri,
Self::InvalidNoProxyRule { .. } => ErrorCode::InvalidNoProxyRule,
Self::InvalidProxyConfig { .. } | Self::ProxyAuthorizationRequiresHttpProxy => {
ErrorCode::InvalidProxyConfig
}
Self::InvalidAdaptiveConcurrencyPolicy { .. } => {
ErrorCode::InvalidAdaptiveConcurrencyPolicy
}
Self::SerializeJson { .. } => ErrorCode::SerializeJson,
Self::SerializeQuery { .. } => ErrorCode::SerializeQuery,
Self::SerializeForm { .. } => ErrorCode::SerializeForm,
Self::RequestBuild { .. } => ErrorCode::RequestBuild,
Self::Transport { .. } => ErrorCode::Transport,
Self::Timeout { .. } => ErrorCode::Timeout,
Self::DeadlineExceeded { .. } => ErrorCode::DeadlineExceeded,
Self::ReadBody { .. } => ErrorCode::ReadBody,
Self::WriteBody { .. } => ErrorCode::WriteBody,
Self::ResponseBodyTooLarge { .. } => ErrorCode::ResponseBodyTooLarge,
Self::HttpStatus { .. } => ErrorCode::HttpStatus,
Self::DeserializeJson { .. } => ErrorCode::DeserializeJson,
Self::DecodeText { .. } => ErrorCode::DecodeText,
Self::InvalidHeaderName { .. } => ErrorCode::InvalidHeaderName,
Self::InvalidHeaderValue { .. } => ErrorCode::InvalidHeaderValue,
Self::DecodeContentEncoding { .. } => ErrorCode::DecodeContentEncoding,
Self::ConcurrencyLimitClosed => ErrorCode::ConcurrencyLimitClosed,
Self::TlsBackendUnavailable { .. } => ErrorCode::TlsBackendUnavailable,
Self::TlsBackendInit { .. } => ErrorCode::TlsBackendInit,
Self::TlsConfig { .. } => ErrorCode::TlsConfig,
Self::RetryBudgetExhausted { .. } => ErrorCode::RetryBudgetExhausted,
Self::CircuitOpen { .. } => ErrorCode::CircuitOpen,
Self::MissingRedirectLocation { .. } => ErrorCode::MissingRedirectLocation,
Self::InvalidRedirectLocation { .. } => ErrorCode::InvalidRedirectLocation,
Self::RedirectLimitExceeded { .. } => ErrorCode::RedirectLimitExceeded,
Self::RedirectBodyNotReplayable { .. } => ErrorCode::RedirectBodyNotReplayable,
}
}
pub const fn status_code(&self) -> Option<u16> {
match self {
Self::HttpStatus { status, .. } => Some(*status),
_ => None,
}
}
pub const fn response_headers(&self) -> Option<&HeaderMap> {
match self {
Self::HttpStatus { headers, .. } => Some(headers),
_ => None,
}
}
pub fn retry_after(&self, now: SystemTime) -> Option<Duration> {
let headers = self.response_headers()?;
parse_retry_after(headers, now)
}
pub fn request_id(&self) -> Option<&str> {
let headers = self.response_headers()?;
headers
.get("x-request-id")
.or_else(|| headers.get("x-amz-request-id"))
.or_else(|| headers.get("x-amz-id-2"))
.and_then(|value| value.to_str().ok())
}
pub fn request_method(&self) -> Option<&Method> {
match self {
Self::Transport { method, .. }
| Self::Timeout { method, .. }
| Self::DeadlineExceeded { method, .. }
| Self::WriteBody { method, .. }
| Self::ResponseBodyTooLarge { method, .. }
| Self::HttpStatus { method, .. }
| Self::DecodeContentEncoding { method, .. }
| Self::RetryBudgetExhausted { method, .. }
| Self::CircuitOpen { method, .. }
| Self::MissingRedirectLocation { method, .. }
| Self::InvalidRedirectLocation { method, .. }
| Self::RedirectLimitExceeded { method, .. }
| Self::RedirectBodyNotReplayable { method, .. } => Some(method),
_ => None,
}
}
pub fn request_uri_redacted(&self) -> Option<&str> {
match self {
Self::InvalidUri { uri }
| Self::InvalidProxyConfig { proxy_uri: uri, .. }
| Self::Transport { uri, .. }
| Self::Timeout { uri, .. }
| Self::DeadlineExceeded { uri, .. }
| Self::WriteBody { uri, .. }
| Self::ResponseBodyTooLarge { uri, .. }
| Self::HttpStatus { uri, .. }
| Self::DecodeContentEncoding { uri, .. }
| Self::RetryBudgetExhausted { uri, .. }
| Self::CircuitOpen { uri, .. }
| Self::MissingRedirectLocation { uri, .. }
| Self::InvalidRedirectLocation { uri, .. }
| Self::RedirectLimitExceeded { uri, .. }
| Self::RedirectBodyNotReplayable { uri, .. } => Some(uri),
_ => None,
}
}
pub fn request_uri_redacted_owned(&self) -> Option<String> {
self.request_uri_redacted().map(ToOwned::to_owned)
}
pub fn request_path(&self) -> Option<String> {
let uri = self.request_uri_redacted()?;
if let Ok(parsed) = uri.parse::<http::Uri>() {
let path = parsed.path();
if !path.is_empty() {
return Some(path.to_owned());
}
}
let without_query = uri.split_once('?').map_or(uri, |(left, _)| left);
let without_fragment = without_query
.split_once('#')
.map_or(without_query, |(left, _)| left);
if without_fragment.is_empty() {
None
} else {
Some(without_fragment.to_owned())
}
}
}