Skip to main content

ntex/client/
error.rs

1//! HTTP client errors.
2use std::{error::Error as StdError, io, ops::Deref, rc::Rc};
3
4use serde_json::error::Error as JsonError;
5
6use crate::error::ErrorDiagnostic;
7use crate::http::error::{DecodeError, EncodeError, HttpError, PayloadError};
8use crate::util::{Either, clone_io_error};
9
10/// Errors that can occur while deserializing a JSON response payload.
11#[derive(thiserror::Error, Debug)]
12pub enum JsonPayloadError {
13    /// The response content type is not JSON.
14    #[error("Content type error")]
15    ContentType,
16    /// The response body could not be deserialized.
17    #[error("Json deserialize error")]
18    Deserialize(#[source] Option<JsonError>),
19    /// The response payload could not be read.
20    #[error("Error occurred while reading payload")]
21    Payload(
22        #[from]
23        #[source]
24        ClientPayloadError,
25    ),
26}
27
28impl Clone for JsonPayloadError {
29    fn clone(&self) -> Self {
30        match self {
31            JsonPayloadError::ContentType => JsonPayloadError::ContentType,
32            JsonPayloadError::Deserialize(_) => JsonPayloadError::Deserialize(None),
33            JsonPayloadError::Payload(err) => JsonPayloadError::Payload(err.clone()),
34        }
35    }
36}
37
38impl From<JsonError> for JsonPayloadError {
39    fn from(err: JsonError) -> JsonPayloadError {
40        JsonPayloadError::Deserialize(Some(err))
41    }
42}
43
44impl From<PayloadError> for JsonPayloadError {
45    fn from(err: PayloadError) -> JsonPayloadError {
46        JsonPayloadError::Payload(ClientPayloadError(err))
47    }
48}
49
50impl ErrorDiagnostic for JsonPayloadError {
51    fn signature(&self) -> &'static str {
52        match self {
53            JsonPayloadError::ContentType => "ntex-client-JsonContentType",
54            JsonPayloadError::Deserialize(_) => "ntex-client-JsonDeserialize",
55            JsonPayloadError::Payload(_) => "ntex-client-JsonPayload",
56        }
57    }
58}
59
60#[derive(thiserror::Error, Clone, Debug)]
61#[error("{0}")]
62/// Error returned while reading a client response payload.
63pub struct ClientPayloadError(
64    #[from]
65    #[source]
66    pub(crate) PayloadError,
67);
68
69impl Deref for ClientPayloadError {
70    type Target = PayloadError;
71
72    fn deref(&self) -> &Self::Target {
73        &self.0
74    }
75}
76
77impl ErrorDiagnostic for ClientPayloadError {
78    fn signature(&self) -> &'static str {
79        "ntex-client-Payload"
80    }
81}
82
83/// Errors that can occur while connecting to an HTTP host.
84#[derive(thiserror::Error, Debug)]
85pub enum ConnectError {
86    /// TLS support is not enabled.
87    #[error("SSL is not supported")]
88    SslIsNotSupported,
89
90    /// The host name could not be resolved.
91    #[error("Failed resolving hostname: {0}")]
92    Resolver(
93        #[from]
94        #[source]
95        io::Error,
96    ),
97
98    /// No DNS records were found.
99    #[error("No dns records found for the input")]
100    NoRecords,
101
102    /// The connector disconnected.
103    #[error("Connector has been disconnected")]
104    Disconnected(#[source] Option<io::Error>),
105
106    /// The connector received invalid input.
107    #[error("Invalid connect input")]
108    InvalidInput,
109
110    /// The connector received an unresolved host name.
111    #[error("Connector received `Connect` method with unresolved host")]
112    Unresolved,
113}
114
115impl ErrorDiagnostic for ConnectError {
116    fn signature(&self) -> &'static str {
117        match self {
118            ConnectError::SslIsNotSupported => "ntex-client-connect-SslIsNotSupported",
119            ConnectError::Resolver(..) => "ntex-client-connect-Resolver",
120            ConnectError::NoRecords => "ntex-client-connect-NoRecords",
121            ConnectError::Disconnected(_) => "ntex-client-connect-Disconnected",
122            ConnectError::InvalidInput => "ntex-client-connect-InvalidInput",
123            ConnectError::Unresolved => "ntex-client-connect-Unresolved",
124        }
125    }
126}
127
128impl Clone for ConnectError {
129    fn clone(&self) -> Self {
130        match self {
131            ConnectError::SslIsNotSupported => ConnectError::SslIsNotSupported,
132            ConnectError::Resolver(e) => ConnectError::Resolver(clone_io_error(e)),
133            ConnectError::NoRecords => ConnectError::NoRecords,
134            ConnectError::Disconnected(e) => {
135                if let Some(e) = e {
136                    ConnectError::Disconnected(Some(clone_io_error(e)))
137                } else {
138                    ConnectError::Disconnected(None)
139                }
140            }
141            ConnectError::InvalidInput => ConnectError::InvalidInput,
142            ConnectError::Unresolved => ConnectError::Unresolved,
143        }
144    }
145}
146
147impl From<crate::connect::ConnectError> for ConnectError {
148    fn from(err: crate::connect::ConnectError) -> ConnectError {
149        match err {
150            crate::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
151            crate::connect::ConnectError::NoRecords => ConnectError::NoRecords,
152            crate::connect::ConnectError::InvalidInput => ConnectError::InvalidInput,
153            crate::connect::ConnectError::Unresolved => ConnectError::Unresolved,
154            crate::connect::ConnectError::Io(e) => ConnectError::Disconnected(Some(e)),
155        }
156    }
157}
158
159#[derive(Copy, Clone, Debug, thiserror::Error)]
160/// Error returned when a request URI is not valid for an HTTP client request.
161pub enum InvalidUrl {
162    /// The URI does not contain a scheme.
163    #[error("Missing url scheme")]
164    MissingScheme,
165    /// The URI uses a scheme other than HTTP, HTTPS, WS, or WSS.
166    #[error("Unknown url scheme")]
167    UnknownScheme,
168    /// The URI does not contain a host.
169    #[error("Missing host name")]
170    MissingHost,
171    /// The URI could not be parsed.
172    #[error("Url parse error: {0}")]
173    Parse(
174        #[from]
175        #[source]
176        urly::InvalidUrl,
177    ),
178}
179
180impl From<std::convert::Infallible> for InvalidUrl {
181    fn from(err: std::convert::Infallible) -> InvalidUrl {
182        match err {}
183    }
184}
185
186/// Errors that can occur while sending a request or reading its response.
187#[derive(Debug, thiserror::Error)]
188pub enum ClientError {
189    /// Invalid URL
190    #[error("Invalid URL: {0}")]
191    Url(
192        #[from]
193        #[source]
194        InvalidUrl,
195    ),
196    /// Failed to connect to host
197    #[error("Failed to connect to host: {0}")]
198    Connect(
199        #[from]
200        #[source]
201        ConnectError,
202    ),
203    /// Error sending request
204    #[error("Error sending request: {0}")]
205    Send(
206        #[from]
207        #[source]
208        io::Error,
209    ),
210    /// Error encoding request
211    #[error("Error during request encoding: {0}")]
212    Request(
213        #[from]
214        #[source]
215        EncodeError,
216    ),
217    /// Error parsing response
218    #[error("Error during response parsing: {0}")]
219    Response(
220        #[from]
221        #[source]
222        DecodeError,
223    ),
224    /// Invalid request header
225    #[error("{0}")]
226    Http(
227        #[from]
228        #[source]
229        HttpError,
230    ),
231    /// Http2 error
232    #[error("Http2 error {0}")]
233    H2(
234        #[from]
235        #[source]
236        ntex_h2::OperationError,
237    ),
238    /// Response took too long
239    #[error("Timeout while waiting for response")]
240    Timeout,
241    /// Other error, for example a request body or query serialization error
242    #[error("{0}")]
243    Error(
244        #[from]
245        #[source]
246        Rc<dyn StdError>,
247    ),
248}
249
250impl Clone for ClientError {
251    fn clone(&self) -> ClientError {
252        match self {
253            ClientError::Url(err) => ClientError::Url(*err),
254            ClientError::Connect(err) => ClientError::Connect(err.clone()),
255            ClientError::Request(err) => ClientError::Request(err.clone()),
256            ClientError::Response(err) => ClientError::Response(*err),
257            ClientError::Http(err) => ClientError::Http(*err),
258            ClientError::H2(err) => ClientError::H2(*err),
259            ClientError::Timeout => ClientError::Timeout,
260            ClientError::Error(err) => ClientError::Error(err.clone()),
261            ClientError::Send(err) => ClientError::Send(crate::util::clone_io_error(err)),
262        }
263    }
264}
265
266impl From<Either<EncodeError, io::Error>> for ClientError {
267    fn from(err: Either<EncodeError, io::Error>) -> Self {
268        match err {
269            Either::Left(err) => ClientError::Request(err),
270            Either::Right(err) => ClientError::Send(err),
271        }
272    }
273}
274
275impl From<Either<DecodeError, io::Error>> for ClientError {
276    fn from(err: Either<DecodeError, io::Error>) -> Self {
277        match err {
278            Either::Left(err) => ClientError::Response(err),
279            Either::Right(err) => ClientError::Send(err),
280        }
281    }
282}
283
284impl ErrorDiagnostic for ClientError {
285    fn signature(&self) -> &'static str {
286        match self {
287            ClientError::Url(_) => "ntex-client-Url",
288            ClientError::Http(_) => "ntex-client-Http",
289            ClientError::Connect(err) => err.signature(),
290            ClientError::Send(err) => err.signature(),
291            ClientError::Request(_) => "ntex-client-Request",
292            ClientError::Response(_) => "ntex-client-Response",
293            ClientError::Timeout => "ntex-client-Timeout",
294            ClientError::Error(_) => "ntex-client-Error",
295            ClientError::H2(err) => err.signature(),
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn connect_error_from() {
306        let err = ConnectError::from(crate::connect::ConnectError::InvalidInput);
307        assert!(matches!(err, ConnectError::InvalidInput));
308        assert_eq!(err.signature(), "ntex-client-connect-InvalidInput");
309        assert!(matches!(err.clone(), ConnectError::InvalidInput));
310    }
311
312    #[test]
313    fn connect_error_conversions() {
314        use crate::connect::ConnectError as Base;
315
316        let cases = [
317            (
318                Base::Resolver(io::Error::other("dns")),
319                "ntex-client-connect-Resolver",
320            ),
321            (Base::NoRecords, "ntex-client-connect-NoRecords"),
322            (Base::Unresolved, "ntex-client-connect-Unresolved"),
323            (
324                Base::Io(io::Error::other("io")),
325                "ntex-client-connect-Disconnected",
326            ),
327        ];
328        for (err, sig) in cases {
329            let err = ConnectError::from(err);
330            assert_eq!(err.signature(), sig);
331            assert_eq!(err.clone().signature(), sig);
332        }
333        for err in [
334            ConnectError::SslIsNotSupported,
335            ConnectError::Disconnected(None),
336        ] {
337            assert_eq!(err.clone().to_string(), err.to_string());
338            assert_eq!(err.clone().signature(), err.signature());
339        }
340
341        let err = ConnectError::Disconnected(Some(io::Error::other("gone")));
342        let ConnectError::Disconnected(Some(e)) = err.clone() else {
343            panic!()
344        };
345        assert_eq!(e.kind(), io::ErrorKind::Other);
346        assert!(e.to_string().contains("gone"), "{e}");
347    }
348
349    #[test]
350    fn client_error_clone_and_signature() {
351        let errs = [
352            (
353                ClientError::from(InvalidUrl::MissingHost),
354                "ntex-client-Url",
355            ),
356            (
357                ClientError::from(ConnectError::NoRecords),
358                "ntex-client-connect-NoRecords",
359            ),
360            (
361                ClientError::from(Either::<EncodeError, io::Error>::Left(
362                    EncodeError::UnexpectedEof,
363                )),
364                "ntex-client-Request",
365            ),
366            (
367                ClientError::from(Either::<DecodeError, io::Error>::Left(DecodeError::Header)),
368                "ntex-client-Response",
369            ),
370            (
371                ClientError::from(HttpError::from(
372                    crate::http::header::HeaderName::try_from("\n").unwrap_err(),
373                )),
374                "ntex-client-Http",
375            ),
376            (ClientError::Timeout, "ntex-client-Timeout"),
377            (
378                ClientError::from(Rc::new(io::Error::other("other")) as Rc<dyn StdError>),
379                "ntex-client-Error",
380            ),
381            (
382                ClientError::from(ntex_h2::OperationError::Disconnected),
383                ntex_h2::OperationError::Disconnected.signature(),
384            ),
385        ];
386        for (err, sig) in errs {
387            assert_eq!(err.signature(), sig, "{err:?}");
388            let cloned = err.clone();
389            assert_eq!(cloned.signature(), sig);
390            if !matches!(err, ClientError::Connect(_)) {
391                assert_eq!(cloned.to_string(), err.to_string());
392            }
393        }
394
395        let io_err = |msg| io::Error::new(io::ErrorKind::BrokenPipe, msg);
396        for (err, msg) in [
397            (
398                ClientError::from(Either::<EncodeError, _>::Right(io_err("write"))),
399                "write",
400            ),
401            (
402                ClientError::from(Either::<DecodeError, _>::Right(io_err("read"))),
403                "read",
404            ),
405        ] {
406            let ClientError::Send(e) = err.clone() else {
407                panic!("{err:?}")
408            };
409            // the cloned io error keeps its kind, the message is not preserved as is
410            assert_eq!(e.kind(), io::ErrorKind::BrokenPipe);
411            assert!(e.to_string().contains(msg), "{e}");
412            assert_eq!(err.signature(), e.signature());
413        }
414    }
415
416    #[test]
417    fn payload_errors() {
418        let err = ClientPayloadError::from(PayloadError::Overflow);
419        assert_eq!(err.signature(), "ntex-client-Payload");
420        assert!(matches!(*err, PayloadError::Overflow));
421
422        let json_err = serde_json::from_str::<u32>("x").unwrap_err();
423        let errs = [
424            (JsonPayloadError::ContentType, "ntex-client-JsonContentType"),
425            (
426                JsonPayloadError::from(json_err),
427                "ntex-client-JsonDeserialize",
428            ),
429            (
430                JsonPayloadError::from(PayloadError::Overflow),
431                "ntex-client-JsonPayload",
432            ),
433        ];
434        for (err, sig) in errs {
435            assert_eq!(err.signature(), sig);
436            assert_eq!(err.clone().signature(), sig);
437        }
438        // the deserialize error is not cloneable
439        let err = JsonPayloadError::from(serde_json::from_str::<u32>("x").unwrap_err());
440        assert!(matches!(err.clone(), JsonPayloadError::Deserialize(None)));
441        assert!(matches!(
442            JsonPayloadError::from(PayloadError::Overflow).clone(),
443            JsonPayloadError::Payload(ClientPayloadError(PayloadError::Overflow))
444        ));
445    }
446}