Skip to main content

nautilus_network/http/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! HTTP client error types.
17
18use std::error::Error;
19
20/// Errors returned by the HTTP client.
21///
22/// Includes generic transport errors, timeouts, and proxy configuration errors.
23#[derive(thiserror::Error, Debug)]
24pub enum HttpClientError {
25    #[error("HTTP error occurred: {0}")]
26    Error(String),
27
28    #[error("HTTP transport error: {0}")]
29    TransportError(String),
30
31    #[error("HTTP request timed out: {0}")]
32    TimeoutError(String),
33
34    #[error("Invalid proxy URL: {0}")]
35    InvalidProxy(String),
36
37    #[error("Failed to build HTTP client: {0}")]
38    ClientBuildError(String),
39}
40
41impl From<String> for HttpClientError {
42    fn from(value: String) -> Self {
43        Self::Error(value)
44    }
45}
46
47pub(super) fn transport_error(e: &(dyn Error + 'static)) -> HttpClientError {
48    let mut message = String::new();
49    let mut cause = Some(e);
50    let mut timed_out = false;
51
52    while let Some(e) = cause {
53        if !message.is_empty() {
54            message.push_str(": ");
55        }
56        message.push_str(&e.to_string());
57
58        timed_out |= e
59            .downcast_ref::<hyper::Error>()
60            .is_some_and(hyper::Error::is_timeout)
61            || e.downcast_ref::<std::io::Error>()
62                .is_some_and(|e| e.kind() == std::io::ErrorKind::TimedOut);
63        cause = e.source();
64    }
65
66    if timed_out {
67        HttpClientError::TimeoutError(message)
68    } else {
69        HttpClientError::TransportError(message)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use std::io;
76
77    use rstest::rstest;
78
79    use super::*;
80
81    #[rstest]
82    #[case::timeout(io::ErrorKind::TimedOut, true)]
83    #[case::refused(io::ErrorKind::ConnectionRefused, false)]
84    fn socket_errors_preserve_classification(#[case] kind: io::ErrorKind, #[case] timeout: bool) {
85        let error = transport_error(&io::Error::new(kind, "socket failure"));
86        match (error, timeout) {
87            (HttpClientError::TimeoutError(message), true)
88            | (HttpClientError::TransportError(message), false) => {
89                assert_eq!(message, "socket failure");
90            }
91            (error, _) => panic!("unexpected classification: {error}"),
92        }
93    }
94}