use std::{error::Error as StdError, fmt, io};
use http::Uri;
use crate::{StatusCode, client::ext::ReasonPhrase, util::Escape};
pub type Result<T> = std::result::Result<T, Error>;
pub type BoxError = Box<dyn StdError + Send + Sync>;
pub struct Error {
inner: Box<Inner>,
}
struct Inner {
kind: Kind,
source: Option<BoxError>,
uri: Option<Uri>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
uri: None,
}),
}
}
pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Builder, Some(e))
}
pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Body, Some(e))
}
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e))
}
pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Decode, Some(e))
}
pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Request, Some(e))
}
pub(crate) fn connect<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connect, Some(e))
}
pub(crate) fn redirect<E: Into<BoxError>>(e: E, uri: Uri) -> Error {
Error::new(Kind::Redirect, Some(e)).with_uri(uri)
}
pub(crate) fn upgrade<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Upgrade, Some(e))
}
#[cfg(feature = "ws-yawc")]
#[allow(dead_code)] pub(crate) fn websocket<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::WebSocket, Some(e))
}
pub(crate) fn status_code(uri: Uri, status: StatusCode, reason: Option<ReasonPhrase>) -> Error {
Error::new(Kind::Status(status, reason), None::<Error>).with_uri(uri)
}
pub(crate) fn uri_bad_scheme(uri: Uri) -> Error {
Error::new(Kind::Builder, Some(BadScheme)).with_uri(uri)
}
#[allow(dead_code)]
pub(crate) fn canceled<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Canceled, Some(e))
}
#[allow(dead_code)]
pub(crate) fn channel_closed<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::ChannelClosed, Some(e))
}
#[allow(dead_code)]
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Io, Some(e))
}
#[allow(dead_code)]
pub(crate) fn body_write<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::BodyWrite, Some(e))
}
#[allow(dead_code)]
pub(crate) fn shutdown<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Shutdown, Some(e))
}
#[allow(dead_code)]
pub(crate) fn http2<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Http2, Some(e))
}
#[allow(dead_code)]
pub(crate) fn proxy_connect<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::ProxyConnect, Some(e))
}
}
impl Error {
pub fn uri(&self) -> Option<&Uri> {
self.inner.uri.as_ref()
}
pub fn uri_mut(&mut self) -> Option<&mut Uri> {
self.inner.uri.as_mut()
}
pub fn with_uri(mut self, uri: Uri) -> Self {
self.inner.uri = Some(uri);
self
}
pub fn without_uri(mut self) -> Self {
self.inner.uri = None;
self
}
pub fn is_builder(&self) -> bool {
matches!(self.inner.kind, Kind::Builder)
}
pub fn is_redirect(&self) -> bool {
matches!(self.inner.kind, Kind::Redirect)
}
pub fn is_status(&self) -> bool {
matches!(self.inner.kind, Kind::Status(_, _))
}
pub fn is_timeout(&self) -> bool {
walk_source_chain(self, |err| {
err.is::<TimedOut>()
|| err
.downcast_ref::<crate::client::CoreError>()
.is_some_and(|e| e.is_timeout())
|| err
.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::TimedOut)
})
}
pub fn is_request(&self) -> bool {
matches!(self.inner.kind, Kind::Request)
}
pub fn is_connect(&self) -> bool {
if matches!(self.inner.kind, Kind::Connect) {
return true;
}
walk_source_chain(self, |err| {
err.downcast_ref::<crate::client::Error>()
.is_some_and(|e| e.is_connect())
})
}
pub fn is_dns(&self) -> bool {
walk_source_chain(self, |err| {
err.downcast_ref::<io::Error>().is_some_and(|e| {
let msg = e.to_string().to_lowercase();
msg.contains("dns") || msg.contains("resolve") || msg.contains("lookup")
})
})
}
pub fn is_proxy_connect(&self) -> bool {
use crate::client::Error;
walk_source_chain(self, |err| {
err.downcast_ref::<Error>()
.is_some_and(|e| e.is_proxy_connect())
})
}
pub fn is_connection_reset(&self) -> bool {
walk_source_chain(self, |err| {
err.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::ConnectionReset)
})
}
pub fn is_body(&self) -> bool {
matches!(self.inner.kind, Kind::Body)
}
pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls)
}
pub fn is_decode(&self) -> bool {
matches!(self.inner.kind, Kind::Decode)
}
pub fn is_upgrade(&self) -> bool {
matches!(self.inner.kind, Kind::Upgrade)
}
pub fn is_canceled(&self) -> bool {
matches!(self.inner.kind, Kind::Canceled)
}
pub fn is_channel_closed(&self) -> bool {
matches!(self.inner.kind, Kind::ChannelClosed)
}
pub fn is_io(&self) -> bool {
matches!(self.inner.kind, Kind::Io)
}
pub fn is_body_write(&self) -> bool {
matches!(self.inner.kind, Kind::BodyWrite)
}
pub fn is_shutdown(&self) -> bool {
matches!(self.inner.kind, Kind::Shutdown)
}
pub fn is_http2(&self) -> bool {
matches!(self.inner.kind, Kind::Http2)
}
pub fn is_proxy_connect_kind(&self) -> bool {
matches!(self.inner.kind, Kind::ProxyConnect)
}
#[cfg(feature = "ws-yawc")]
#[allow(dead_code)] pub fn is_websocket(&self) -> bool {
matches!(self.inner.kind, Kind::WebSocket)
}
pub fn status(&self) -> Option<StatusCode> {
match self.inner.kind {
Kind::Status(code, _) => Some(code),
_ => None,
}
}
}
fn walk_source_chain(
e: &dyn StdError,
predicate: impl Fn(&(dyn StdError + 'static)) -> bool,
) -> bool {
let mut source = e.source();
while let Some(err) = source {
if predicate(err) {
return true;
}
source = err.source();
}
false
}
#[inline]
pub(crate) fn map_timeout_to_connector_error(error: BoxError) -> BoxError {
if error.is::<tower::timeout::error::Elapsed>() {
Box::new(TimedOut)
} else {
error
}
}
#[inline]
pub(crate) fn map_timeout_to_request_error(error: BoxError) -> BoxError {
if error.is::<tower::timeout::error::Elapsed>() {
Box::new(Error::request(TimedOut))
} else {
error
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut builder = f.debug_struct("hpx::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref uri) = self.inner.uri {
builder.field("uri", uri);
}
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.inner.kind {
Kind::Builder => f.write_str("builder error")?,
Kind::Request => f.write_str("error sending request")?,
Kind::Connect => f.write_str("connection error")?,
Kind::Body => f.write_str("request or response body error")?,
Kind::Tls => f.write_str("tls error")?,
Kind::Decode => f.write_str("error decoding response body")?,
Kind::Redirect => f.write_str("error following redirect")?,
Kind::Upgrade => f.write_str("error upgrading connection")?,
#[cfg(feature = "ws-yawc")]
Kind::WebSocket => f.write_str("websocket error")?,
Kind::Canceled => f.write_str("request canceled")?,
Kind::ChannelClosed => f.write_str("channel closed")?,
Kind::Io => f.write_str("I/O error")?,
Kind::BodyWrite => f.write_str("error writing body")?,
Kind::Shutdown => f.write_str("error shutting down connection")?,
Kind::Http2 => f.write_str("HTTP/2 error")?,
Kind::ProxyConnect => f.write_str("proxy connect error")?,
Kind::Status(ref code, ref reason) => {
let prefix = if code.is_client_error() {
"HTTP status client error"
} else {
debug_assert!(code.is_server_error());
"HTTP status server error"
};
if let Some(reason) = reason {
write!(
f,
"{prefix} ({} {})",
code.as_str(),
Escape::new(reason.as_bytes())
)?;
} else {
write!(f, "{prefix} ({code})")?;
}
}
};
if let Some(uri) = &self.inner.uri {
write!(f, " for uri ({})", uri)?;
}
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.inner.source.as_ref().map(|e| &**e as _)
}
}
impl From<crate::client::CoreError> for Error {
fn from(e: crate::client::CoreError) -> Self {
if e.is_canceled() {
Error::canceled(e)
} else if e.is_closed() {
Error::channel_closed(e)
} else if e.is_timeout() {
Error::request(TimedOut)
} else {
Error::request(e)
}
}
}
impl From<crate::client::Error> for Error {
fn from(e: crate::client::Error) -> Self {
if e.is_connect() {
Error::connect(e)
} else if e.is_proxy_connect() {
Error::proxy_connect(e)
} else {
Error::request(e)
}
}
}
#[derive(Debug)]
pub(crate) enum Kind {
Builder,
Request,
Connect,
Tls,
Redirect,
Status(StatusCode, Option<ReasonPhrase>),
Body,
Decode,
Upgrade,
#[cfg(feature = "ws-yawc")]
#[allow(dead_code)] WebSocket,
#[allow(dead_code)]
Canceled,
#[allow(dead_code)]
ChannelClosed,
#[allow(dead_code)]
Io,
#[allow(dead_code)]
BodyWrite,
#[allow(dead_code)]
Shutdown,
#[allow(dead_code)]
Http2,
#[allow(dead_code)]
ProxyConnect,
}
#[derive(Debug)]
pub(crate) struct TimedOut;
#[derive(Debug)]
pub(crate) struct BadScheme;
#[derive(Debug)]
pub(crate) struct ProxyConnect(pub(crate) BoxError);
impl fmt::Display for TimedOut {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("operation timed out")
}
}
impl StdError for TimedOut {}
impl fmt::Display for BadScheme {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("URI scheme is not allowed")
}
}
impl StdError for BadScheme {}
impl fmt::Display for ProxyConnect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "proxy connect error: {}", self.0)
}
}
impl StdError for ProxyConnect {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&*self.0)
}
}
#[cfg(feature = "http3")]
#[derive(Debug)]
pub enum H3Error {
Handshake {
source: quinn::ConnectionError,
},
Framing {
source: hpx_h3::error::ConnectionError,
},
StreamReset {
code: u64,
stream_id: u64,
},
IdleClose,
ZeroRttRejected,
MigrationFailed,
VersionNegotiationFailed,
FlowControl,
MaxConcurrentStreamsExceeded,
GoAwayDrained,
AltSvcUnreachable,
Other(Box<dyn StdError + Send + Sync>),
}
#[cfg(feature = "http3")]
impl fmt::Display for H3Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
H3Error::Handshake { source } => {
write!(f, "QUIC handshake failed: {source:?}")
}
H3Error::Framing { source } => write!(f, "HTTP/3 framing error: {source}"),
H3Error::StreamReset { code, stream_id } => {
write!(
f,
"HTTP/3 stream reset (code={code:#x}, stream={stream_id})"
)
}
H3Error::IdleClose => write!(f, "HTTP/3 connection closed while idle"),
H3Error::ZeroRttRejected => write!(f, "HTTP/3 0-RTT rejected"),
H3Error::MigrationFailed => write!(f, "HTTP/3 connection migration failed"),
H3Error::VersionNegotiationFailed => {
write!(f, "HTTP/3 version negotiation failed")
}
H3Error::FlowControl => write!(f, "HTTP/3 flow control error"),
H3Error::MaxConcurrentStreamsExceeded => {
write!(f, "HTTP/3 max concurrent streams exceeded")
}
H3Error::GoAwayDrained => write!(f, "HTTP/3 connection drained (GOAWAY)"),
H3Error::AltSvcUnreachable => write!(f, "HTTP/3 Alt-Svc unreachable"),
H3Error::Other(err) => write!(f, "HTTP/3 connector error: {err}"),
}
}
}
#[cfg(feature = "http3")]
impl StdError for H3Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
H3Error::Handshake { source } => Some(source),
H3Error::Framing { source } => Some(source),
H3Error::Other(err) => Some(&**err),
H3Error::StreamReset { .. }
| H3Error::IdleClose
| H3Error::ZeroRttRejected
| H3Error::MigrationFailed
| H3Error::VersionNegotiationFailed
| H3Error::FlowControl
| H3Error::MaxConcurrentStreamsExceeded
| H3Error::GoAwayDrained
| H3Error::AltSvcUnreachable => None,
}
}
}
#[cfg(feature = "http3")]
impl H3Error {
#[allow(dead_code)] pub(crate) fn is_stop_sending(&self, code: u64) -> bool {
matches!(self, H3Error::StreamReset { code: c, .. } if *c == code)
}
}
#[cfg(feature = "http3")]
impl From<quinn::ConnectError> for H3Error {
fn from(err: quinn::ConnectError) -> Self {
H3Error::Other(Box::new(err))
}
}
#[cfg(feature = "http3")]
impl From<Box<dyn StdError + Send + Sync>> for H3Error {
fn from(err: Box<dyn StdError + Send + Sync>) -> Self {
H3Error::Other(err)
}
}
#[cfg(feature = "http3")]
impl From<H3Error> for Error {
fn from(err: H3Error) -> Error {
match err {
H3Error::Handshake { .. }
| H3Error::IdleClose
| H3Error::ZeroRttRejected
| H3Error::MigrationFailed
| H3Error::VersionNegotiationFailed
| H3Error::GoAwayDrained
| H3Error::AltSvcUnreachable => Error::new(Kind::Connect, Some(err)),
H3Error::Framing { .. } | H3Error::MaxConcurrentStreamsExceeded => {
Error::new(Kind::Request, Some(err))
}
H3Error::StreamReset { .. } | H3Error::FlowControl => Error::new(Kind::Body, Some(err)),
H3Error::Other(_) => Error::new(Kind::Request, Some(err)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
impl super::Error {
fn into_io(self) -> io::Error {
io::Error::other(self)
}
}
fn decode_io(e: io::Error) -> Error {
if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
*e.into_inner()
.expect("io::Error::get_ref was Some(_)")
.downcast::<Error>()
.expect("StdError::is() was true")
} else {
Error::decode(e)
}
}
#[test]
fn test_source_chain() {
let root = Error::new(Kind::Request, None::<Error>);
assert!(root.source().is_none());
let link = Error::body(root);
assert!(link.source().is_some());
assert_send::<Error>();
assert_sync::<Error>();
}
#[test]
fn mem_size_of() {
use std::mem::size_of;
assert_eq!(size_of::<Error>(), size_of::<usize>());
}
#[test]
fn roundtrip_io_error() {
let orig = Error::request("orig");
let io = orig.into_io();
let err = decode_io(io);
match err.inner.kind {
Kind::Request => (),
_ => panic!("{err:?}"),
}
}
#[test]
fn from_unknown_io_error() {
let orig = io::Error::other("orly");
let err = decode_io(orig);
match err.inner.kind {
Kind::Decode => (),
_ => panic!("{err:?}"),
}
}
#[test]
fn is_timeout() {
let err = Error::request(super::TimedOut);
assert!(err.is_timeout());
let io = io::Error::from(io::ErrorKind::TimedOut);
let nested = Error::request(io);
assert!(nested.is_timeout());
}
#[test]
fn is_timeout_nested_3_levels() {
let inner = Error::request(super::TimedOut);
let io = io::Error::other(inner);
let outer = Error::request(io);
assert!(outer.is_timeout());
}
#[test]
fn is_connection_reset() {
let err = Error::request(io::Error::new(
io::ErrorKind::ConnectionReset,
"connection reset",
));
assert!(err.is_connection_reset());
let io = io::Error::other(err);
let nested = Error::request(io);
assert!(nested.is_connection_reset());
}
#[test]
fn is_connect_direct() {
let err = Error::connect("connection refused");
assert!(err.is_connect());
}
#[test]
fn is_dns_direct() {
let err = Error::request(io::Error::new(
io::ErrorKind::NotFound,
"dns resolution failed",
));
assert!(err.is_dns());
}
#[test]
fn is_dns_nested() {
let inner = io::Error::new(io::ErrorKind::Other, "resolve lookup failed for host");
let wrapper = io::Error::other(inner);
let err = Error::request(wrapper);
assert!(err.is_dns());
}
#[test]
fn is_dns_no_match() {
let err = Error::request(io::Error::new(
io::ErrorKind::ConnectionRefused,
"connection refused",
));
assert!(!err.is_dns());
}
#[cfg(feature = "http3")]
mod h3_error_tests {
use super::*;
#[test]
fn h3_error_handshake_is_connect() {
let err: Error = H3Error::Handshake {
source: quinn::ConnectionError::TimedOut,
}
.into();
assert!(err.is_connect(), "Handshake must map to is_connect()");
}
#[test]
fn h3_error_stream_reset_is_body() {
let err: Error = H3Error::StreamReset {
code: 0,
stream_id: 0,
}
.into();
assert!(
err.is_body(),
"StreamReset (mid-response) must map to is_body()"
);
}
#[test]
fn h3_error_idle_close_is_connect() {
let err: Error = H3Error::IdleClose.into();
assert!(err.is_connect(), "IdleClose must map to is_connect()");
}
#[test]
fn h3_error_zero_rtt_rejected_is_connect() {
let err: Error = H3Error::ZeroRttRejected.into();
assert!(err.is_connect(), "ZeroRttRejected must map to is_connect()");
}
#[test]
fn h3_error_migration_failed_is_connect() {
let err: Error = H3Error::MigrationFailed.into();
assert!(err.is_connect(), "MigrationFailed must map to is_connect()");
}
#[test]
fn h3_error_version_negotiation_failed_is_connect() {
let err: Error = H3Error::VersionNegotiationFailed.into();
assert!(
err.is_connect(),
"VersionNegotiationFailed must map to is_connect()"
);
}
#[test]
fn h3_error_goaway_drained_is_connect() {
let err: Error = H3Error::GoAwayDrained.into();
assert!(err.is_connect(), "GoAwayDrained must map to is_connect()");
}
#[test]
fn h3_error_alt_svc_unreachable_is_connect() {
let err: Error = H3Error::AltSvcUnreachable.into();
assert!(
err.is_connect(),
"AltSvcUnreachable must map to is_connect()"
);
}
#[test]
fn h3_error_flow_control_is_body() {
let err: Error = H3Error::FlowControl.into();
assert!(err.is_body(), "FlowControl must map to is_body()");
}
#[test]
fn h3_error_max_concurrent_streams_exceeded_is_request() {
let err: Error = H3Error::MaxConcurrentStreamsExceeded.into();
assert!(
err.is_request(),
"MaxConcurrentStreamsExceeded must map to is_request()"
);
}
#[test]
fn h3_error_other_is_request() {
let err: Error = H3Error::Other("connector I/O error".into()).into();
assert!(
err.is_request(),
"Other (catch-all) must map to is_request()"
);
}
}
}