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}