Skip to main content

nexus_net/rest/
error.rs

1//! REST client error types.
2//!
3//! ## TLS error handling
4//!
5//! `From<TlsError> for RestError` partially preserves the TLS layer:
6//! non-IO `TlsError` variants (decrypt failure, peer alert, malformed
7//! record) surface as [`RestError::Tls`]; `TlsError::Io` flattens to
8//! [`RestError::Io`] because it represents a genuine `io::Error` that
9//! happened during TLS operations and the underlying async transport
10//! ([`WireStream`](crate::WireStream)) returns `io::Result` either
11//! way. The original `TlsError::Io` is preserved as the source of the
12//! resulting `io::Error` and reachable via `io_err.source()` /
13//! `io_err.get_ref()`. See [`RestError::Tls`] for the full
14//! sync-vs-async asymmetry note.
15
16use std::fmt;
17
18use crate::http::HttpError;
19
20/// REST client error.
21#[derive(Debug)]
22pub enum RestError {
23    /// I/O error.
24    Io(std::io::Error),
25    /// HTTP protocol error.
26    Http(HttpError),
27    /// Response body exceeds max size.
28    BodyTooLarge {
29        /// Size reported by Content-Length (or accumulated for chunked).
30        size: usize,
31        /// Configured maximum body size in bytes.
32        max: usize,
33    },
34    /// Request exceeds WriteBuf capacity.
35    RequestTooLarge {
36        /// Capacity of the write buffer in bytes.
37        capacity: usize,
38    },
39    /// Header name/value or query parameter contains CR/LF bytes.
40    CrlfInjection,
41    /// Connection is poisoned after an I/O error mid-response.
42    ConnectionPoisoned,
43    /// Read timed out waiting for response.
44    ReadTimeout,
45    /// Connection is stale (dead socket detected after timeout).
46    ConnectionStale,
47    /// Connection closed before response complete.
48    ConnectionClosed(&'static str),
49    /// Invalid URL.
50    InvalidUrl(String),
51    /// `https://` URL used without the `tls` feature enabled.
52    TlsNotEnabled,
53    /// TLS error during connection setup (handshake, certificate
54    /// validation, hostname resolution).
55    ///
56    /// **Steady-state TLS protocol errors** (decrypt failure, peer
57    /// alert, malformed record received during a request) on the
58    /// async `nexus-async-net` paths surface as
59    /// [`RestError::Io`](Self::Io) instead — the underlying
60    /// [`TlsError`](crate::tls::TlsError) is wrapped via
61    /// `io::Error::other` and reachable via `io_err.source()` or
62    /// `io_err.get_ref()`. This asymmetry stems from the
63    /// `WireStream` trait returning `io::Result` for poll
64    /// methods. Sync REST surfaces `Tls` directly because its
65    /// `TlsStream` exposes `TlsError` natively. Pattern-match on
66    /// both `Io` and `Tls` if you need to distinguish TLS-protocol
67    /// failures from generic transport failures across both
68    /// surfaces.
69    #[cfg(feature = "tls")]
70    Tls(crate::tls::TlsError),
71}
72
73impl fmt::Display for RestError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Io(e) => write!(f, "I/O error: {e}"),
77            Self::Http(e) => write!(f, "HTTP error: {e}"),
78            Self::BodyTooLarge { size, max } => {
79                write!(f, "response body too large: {size} bytes (max: {max})")
80            }
81            Self::RequestTooLarge { capacity } => {
82                write!(
83                    f,
84                    "request exceeds write buffer capacity ({capacity} bytes)"
85                )
86            }
87            Self::CrlfInjection => {
88                write!(f, "header or query parameter contains CR/LF")
89            }
90            Self::ConnectionPoisoned => write!(f, "connection poisoned after I/O error"),
91            Self::ReadTimeout => write!(f, "read timed out waiting for response"),
92            Self::ConnectionStale => write!(f, "connection stale (dead socket)"),
93            Self::TlsNotEnabled => write!(f, "https:// requires the `tls` feature"),
94            Self::ConnectionClosed(ctx) => write!(f, "connection closed: {ctx}"),
95            Self::InvalidUrl(u) => write!(f, "invalid URL: {u}"),
96            #[cfg(feature = "tls")]
97            Self::Tls(e) => write!(f, "TLS error: {e}"),
98        }
99    }
100}
101
102impl std::error::Error for RestError {
103    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
104        match self {
105            Self::Io(e) => Some(e),
106            Self::Http(e) => Some(e),
107            #[cfg(feature = "tls")]
108            Self::Tls(e) => Some(e),
109            _ => None,
110        }
111    }
112}
113
114impl From<std::io::Error> for RestError {
115    fn from(e: std::io::Error) -> Self {
116        Self::Io(e)
117    }
118}
119
120impl From<HttpError> for RestError {
121    fn from(e: HttpError) -> Self {
122        Self::Http(e)
123    }
124}
125
126#[cfg(feature = "tls")]
127impl From<crate::tls::TlsError> for RestError {
128    fn from(e: crate::tls::TlsError) -> Self {
129        match e {
130            crate::tls::TlsError::Io(io) => Self::Io(io),
131            other => Self::Tls(other),
132        }
133    }
134}