Skip to main content

base_http_client/
error.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use std::{fmt, io, net::SocketAddr, sync::Arc};
5
6/// A client failure. A request body or interim observer that fails is reported as `Body`, never as a `Protocol` error. A response body that fails with a client error reports it as the source of a `BodyError`, or as the original `BodyError` when a request body failed.
7///
8/// `Negotiation`, `Pool`, `Protocol` and `Body` wrap another layer's error transparently: they display it and report its source. `Connect` and `Io` describe the failure and report the I/O error as their source. A cause chain therefore never repeats a message.
9#[derive(Clone, Debug)]
10pub enum Error {
11    /// Opening the transport to `address` failed, including the TLS handshake and server verification, so no request was sent. The source can also be a local failure, such as exhausted file descriptors or an invalid TLS server name. Cancellation while connecting, including a caller's deadline, is `Cancelled`. A rejected protocol selection is `Negotiation`, which also precedes any request. With TLS 1.3, a server that rejects the client after the handshake is reported by the first exchange instead.
12    Connect {
13        address: SocketAddr,
14        source: Arc<io::Error>,
15    },
16    Negotiation(base_http_protocol::NegotiationError),
17    Closed,
18    Busy,
19    Cancelled,
20    Abandoned,
21    InvalidOptions,
22    Unsupported(&'static str),
23    Pool(base_pool::Error),
24    Protocol(Arc<base_http1::Error>),
25    Io(Arc<io::Error>),
26    Body(base_http::body::BodyError),
27}
28impl fmt::Display for Error {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Connect { address, .. } => write!(f, "connecting to {address} failed"),
32            Self::Negotiation(error) => error.fmt(f),
33            Self::Closed => f.write_str("HTTP client is closed"),
34            Self::Busy => f.write_str("HTTP client request limit reached"),
35            Self::Cancelled => f.write_str("HTTP client request cancelled"),
36            Self::Abandoned => f.write_str("HTTP client response abandoned"),
37            Self::InvalidOptions => f.write_str("invalid HTTP client limits"),
38            Self::Unsupported(reason) => write!(f, "unsupported HTTP client operation: {reason}"),
39            Self::Pool(error) => error.fmt(f),
40            Self::Protocol(error) => error.fmt(f),
41            Self::Io(_) => f.write_str("HTTP client I/O failed"),
42            Self::Body(error) => error.fmt(f),
43        }
44    }
45}
46impl std::error::Error for Error {
47    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48        match self {
49            Self::Connect { source, .. } => Some(source.as_ref()),
50            Self::Negotiation(error) => error.source(),
51            Self::Pool(error) => error.source(),
52            Self::Protocol(error) => error.source(),
53            Self::Io(error) => Some(error.as_ref()),
54            Self::Body(error) => error.source(),
55            _ => None,
56        }
57    }
58}
59impl Error {
60    /// Classify a transport establishment failure, preserving cancellation.
61    pub(crate) fn connect(address: SocketAddr, error: base_scheduler::OperationError) -> Self {
62        match error {
63            base_scheduler::OperationError::Cancelled(_) => Self::Cancelled,
64            base_scheduler::OperationError::Io(error) => Self::Connect {
65                address,
66                source: Arc::new(error),
67            },
68        }
69    }
70}
71
72impl Error {
73    /// Classify a failure of a caller's body or interim observer. A relayed client body failure is kept as is, so it is reported once.
74    pub(crate) fn body<E: std::error::Error + Send + Sync + 'static>(error: E) -> Self {
75        let mut error = Some(error);
76        if let Some(Some(Self::Body(body))) =
77            (&mut error as &mut dyn std::any::Any).downcast_mut::<Option<Self>>()
78        {
79            return Self::Body(body.clone());
80        }
81        Self::Body(base_http::body::BodyError::new(error.unwrap()))
82    }
83}
84
85impl From<base_pool::Error> for Error {
86    fn from(error: base_pool::Error) -> Self {
87        match error {
88            base_pool::Error::Cancelled => Self::Cancelled,
89            base_pool::Error::Closed => Self::Closed,
90            error => Self::Pool(error),
91        }
92    }
93}
94impl From<base_http1::Error> for Error {
95    fn from(error: base_http1::Error) -> Self {
96        match error {
97            base_http1::Error::Cancelled => Self::Cancelled,
98            // The codec reports a failed request body or interim observer as its own body error, but the client classified that failure already.
99            base_http1::Error::Body(error) => match error.downcast::<Self>() {
100                Ok(error) => *error,
101                Err(error) => match error.downcast::<base_http::InterimError<Self>>() {
102                    Ok(error) => match *error {
103                        base_http::InterimError::Handler(error) => error,
104                        error => Self::Protocol(Arc::new(base_http1::Error::Body(Box::new(error)))),
105                    },
106                    Err(error) => Self::Protocol(Arc::new(base_http1::Error::Body(error))),
107                },
108            },
109            error => Self::Protocol(Arc::new(error)),
110        }
111    }
112}
113impl From<io::Error> for Error {
114    fn from(error: io::Error) -> Self {
115        Self::Io(Arc::new(error))
116    }
117}
118impl From<base_scheduler::OperationError> for Error {
119    fn from(error: base_scheduler::OperationError) -> Self {
120        base_http1::Error::from(error).into()
121    }
122}
123impl From<base_http::body::BodyError> for Error {
124    fn from(error: base_http::body::BodyError) -> Self {
125        Self::Body(error)
126    }
127}
128
129impl From<base_http_protocol::NegotiationError> for Error {
130    fn from(error: base_http_protocol::NegotiationError) -> Self {
131        Self::Negotiation(error)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    fn chain(error: &dyn std::error::Error) -> Vec<String> {
140        let mut messages = vec![error.to_string()];
141        let mut source = error.source();
142        while let Some(error) = source {
143            messages.push(error.to_string());
144            source = error.source();
145        }
146        messages
147    }
148
149    fn refused() -> io::Error {
150        io::Error::from(io::ErrorKind::ConnectionRefused)
151    }
152
153    #[test]
154    fn cause_chains_never_repeat_a_message() {
155        let errors = [
156            Error::from(base_http1::Error::Io(refused())),
157            Error::from(refused()),
158            Error::from(base_http::body::BodyError::new(refused())),
159            Error::from(base_http::body::BodyError::new(base_http1::Error::Io(
160                refused(),
161            ))),
162            // A request body relaying another body's failure.
163            Error::from(base_http::body::BodyError::new(
164                base_http::body::BodyError::new(refused()),
165            )),
166            // A failed interim response observer.
167            Error::from(base_http1::Error::Body(Box::new(
168                base_http::InterimError::Handler(Error::from(base_http::body::BodyError::new(
169                    refused(),
170                ))),
171            ))),
172            // A client body relaying another client's failed body.
173            Error::body(Error::body(refused())),
174            Error::Pool(base_pool::Error::WaitersFull),
175            Error::connect("127.0.0.1:9".parse().unwrap(), refused().into()),
176            Error::from(base_http_protocol::NegotiationError::Missing),
177        ];
178        let repeating: Vec<_> = errors
179            .iter()
180            .map(|error| chain(error))
181            .filter(|messages| {
182                let unique: std::collections::HashSet<_> = messages.iter().collect();
183                unique.len() != messages.len()
184            })
185            .collect();
186        assert_eq!(repeating, Vec::<Vec<String>>::new());
187    }
188
189    #[test]
190    fn connect_errors_name_the_address() {
191        let error = Error::connect("10.0.0.5:9292".parse().unwrap(), refused().into());
192        assert_eq!(
193            chain(&error),
194            ["connecting to 10.0.0.5:9292 failed", "connection refused"]
195        );
196        let cancelled = base_scheduler::OperationError::Cancelled(base_scheduler::Cancelled);
197        let error = Error::connect("10.0.0.5:9292".parse().unwrap(), cancelled);
198        assert_eq!(chain(&error), ["HTTP client request cancelled"]);
199    }
200
201    #[test]
202    fn codec_body_failures_keep_the_client_classification() {
203        let upload = base_http1::Error::Body(Box::new(Error::body(refused())));
204        assert_eq!(
205            chain(&Error::from(upload)),
206            ["body failed", "connection refused"]
207        );
208        let observer = base_http1::Error::Body(Box::new(base_http::InterimError::Handler(
209            Error::body(refused()),
210        )));
211        assert_eq!(
212            chain(&Error::from(observer)),
213            ["body failed", "connection refused"]
214        );
215        let finished =
216            base_http1::Error::Body(Box::new(base_http::InterimError::<Error>::Finished));
217        assert_eq!(
218            chain(&Error::from(finished)),
219            ["HTTP/1 body failed", "interim responses have finished"]
220        );
221        let other = base_http1::Error::Body(Box::new(refused()));
222        assert_eq!(
223            chain(&Error::from(other)),
224            ["HTTP/1 body failed", "connection refused"]
225        );
226    }
227
228    #[test]
229    fn protocol_errors_are_transparent() {
230        let error = Error::from(base_http1::Error::Io(refused()));
231        assert_eq!(
232            chain(&error),
233            ["HTTP/1 connection I/O failed", "connection refused"]
234        );
235    }
236}