Skip to main content

icap_rs/
error.rs

1//! Error types for the ICAP crate.
2//!
3//! The error surface is grouped by concern rather than dumped into a single
4//! flat enum. This keeps matches concise and makes it possible to reason about
5//! categories of failures (e.g. "is this a timeout?", "is this retryable?")
6//! without enumerating every variant.
7//!
8//! # Layout
9//!
10//! - [`enum@Error`] is the top-level type returned from public APIs.
11//! - [`TimeoutError`] + [`TimeoutKind`] describe deadline violations.
12//! - [`ProtocolError`] + [`ProtocolField`] describe wire-protocol failures.
13//! - [`ConfigError`] describes builder/setup mistakes.
14//! - [`crate::tls::TlsError`] describes TLS-specific errors (when the
15//!   `tls-rustls` feature is enabled).
16//!
17//! See [`Error::is_timeout`], [`Error::is_io`], [`Error::is_retryable`] for
18//! convenient classifiers.
19//!
20//! [`HandlerError`](crate::HandlerError) is a *separate* type returned from
21//! user handlers — it is not part of this enum.
22
23use http::header::{InvalidHeaderName, InvalidHeaderValue};
24use http::{Error as HttpError, header::ToStrError};
25use std::error::Error as StdError;
26use std::fmt;
27use std::io;
28use std::str::Utf8Error;
29use std::string::FromUtf8Error;
30use std::time::Duration;
31use thiserror::Error;
32
33/// Boxed `std::error::Error` used by [`Error::External`].
34pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
35
36/// Top-level error type.
37#[derive(Debug, Error)]
38#[non_exhaustive]
39pub enum Error {
40    /// Transport-level I/O error (TCP read/write/connect).
41    #[error("I/O error: {0}")]
42    Io(#[source] io::Error),
43
44    /// Operation exceeded a configured deadline.
45    #[error(transparent)]
46    Timeout(#[from] TimeoutError),
47
48    /// Wire-protocol failure (parsing, headers, encapsulation, etc.).
49    #[error(transparent)]
50    Protocol(#[from] ProtocolError),
51
52    /// Builder / configuration failure (invalid service options, unknown
53    /// alias, missing handlers, …).
54    #[error(transparent)]
55    Config(#[from] ConfigError),
56
57    /// TLS layer error (handshake, certificate verification, PEM loading…).
58    #[cfg(feature = "tls-rustls")]
59    #[error(transparent)]
60    Tls(#[from] crate::tls::TlsError),
61
62    /// Error returned by the `http` crate builders.
63    #[error("HTTP builder error: {0}")]
64    Http(#[from] HttpError),
65
66    /// Error from outside this crate, preserved via [`ToIcapResult`] or
67    /// [`Error::external`]. The original error is available through `source()`.
68    #[error("external error: {0}")]
69    External(#[source] BoxError),
70}
71
72impl Error {
73    /// True if the error originates from an I/O failure on the transport.
74    pub const fn is_io(&self) -> bool {
75        matches!(self, Self::Io(_))
76    }
77
78    /// True if the error originates from a deadline (any [`TimeoutKind`]).
79    pub const fn is_timeout(&self) -> bool {
80        matches!(self, Self::Timeout(_))
81    }
82
83    /// True if the error came from the wire protocol layer.
84    pub const fn is_protocol(&self) -> bool {
85        matches!(self, Self::Protocol(_))
86    }
87
88    /// True if the peer sent an embedded body larger than a configured limit.
89    pub const fn is_body_too_large(&self) -> bool {
90        matches!(self, Self::Protocol(ProtocolError::BodyTooLarge { .. }))
91    }
92
93    /// True if the error came from builder / configuration validation.
94    pub const fn is_config(&self) -> bool {
95        matches!(self, Self::Config(_))
96    }
97
98    /// True for the "peer closed before headers" case — common on
99    /// kept-alive connections that the server has idle-closed.
100    pub const fn is_early_close(&self) -> bool {
101        matches!(self, Self::Protocol(ProtocolError::EarlyClose))
102    }
103
104    /// True if a fresh attempt is likely to succeed.
105    ///
106    /// Conservative: connection-establishment timeouts, idle keep-alive
107    /// closure, and `EarlyClose` are considered retryable. Application-level
108    /// protocol errors are *not* retryable — the peer will reject the same
109    /// bytes again.
110    pub const fn is_retryable(&self) -> bool {
111        if self.is_early_close() {
112            return true;
113        }
114        if let Self::Timeout(t) = self {
115            return matches!(t.kind, TimeoutKind::ClientConnect | TimeoutKind::ServerIdle);
116        }
117        false
118    }
119
120    /// Build a protocol parse error.
121    pub fn parse(message: impl Into<String>) -> Self {
122        Self::Protocol(ProtocolError::Parse(message.into()))
123    }
124
125    /// Build an embedded-HTTP parse error.
126    pub fn http_parse(message: impl Into<String>) -> Self {
127        Self::Protocol(ProtocolError::HttpParse(message.into()))
128    }
129
130    /// Build a generic header error.
131    pub fn header(message: impl Into<String>) -> Self {
132        Self::Protocol(ProtocolError::Header(message.into()))
133    }
134
135    /// Build a body error.
136    pub fn body(message: impl Into<String>) -> Self {
137        Self::Protocol(ProtocolError::Body(message.into()))
138    }
139
140    /// Build an oversized-body protocol error.
141    pub const fn body_too_large(size: usize, max: usize) -> Self {
142        Self::Protocol(ProtocolError::BodyTooLarge { size, max })
143    }
144
145    /// Build a serialization error.
146    pub fn serialization(message: impl Into<String>) -> Self {
147        Self::Protocol(ProtocolError::Serialization(message.into()))
148    }
149
150    /// Build a service / configuration error.
151    pub fn service(message: impl Into<String>) -> Self {
152        Self::Config(ConfigError::Other(message.into()))
153    }
154
155    /// Build a "missing required header" protocol error.
156    pub const fn missing_header(name: &'static str) -> Self {
157        Self::Protocol(ProtocolError::MissingHeader(name))
158    }
159
160    /// Build an "invalid `ISTag`" protocol error.
161    pub fn invalid_istag(value: impl Into<String>) -> Self {
162        Self::Protocol(ProtocolError::InvalidISTag(value.into()))
163    }
164
165    /// Build an "invalid status code" protocol error.
166    pub fn invalid_status_code(value: impl Into<String>) -> Self {
167        Self::Protocol(ProtocolError::invalid(ProtocolField::StatusCode, value))
168    }
169
170    /// Build an "invalid method" protocol error.
171    pub fn invalid_method(value: impl Into<String>) -> Self {
172        Self::Protocol(ProtocolError::invalid(ProtocolField::Method, value))
173    }
174
175    /// Build an "invalid URI" protocol error.
176    pub fn invalid_uri(value: impl Into<String>) -> Self {
177        Self::Protocol(ProtocolError::invalid(ProtocolField::Uri, value))
178    }
179
180    /// Build an "invalid protocol version" protocol error.
181    pub fn invalid_version(value: impl Into<String>) -> Self {
182        Self::Protocol(ProtocolError::invalid(ProtocolField::Version, value))
183    }
184
185    /// Build an `External` error from any `std::error::Error`.
186    pub fn external<E>(err: E) -> Self
187    where
188        E: StdError + Send + Sync + 'static,
189    {
190        Self::External(Box::new(err))
191    }
192
193    /// Build an `External` error from an ad-hoc message string.
194    pub fn unexpected(message: impl Into<String>) -> Self {
195        Self::External(Box::<MessageError>::new(MessageError(message.into())))
196    }
197
198    /// Build a `Timeout` from a kind + duration.
199    pub const fn timeout(kind: TimeoutKind, duration: Duration) -> Self {
200        Self::Timeout(TimeoutError { kind, duration })
201    }
202
203    pub const fn client_total_timeout(d: Duration) -> Self {
204        Self::timeout(TimeoutKind::ClientTotal, d)
205    }
206    pub const fn client_connect_timeout(d: Duration) -> Self {
207        Self::timeout(TimeoutKind::ClientConnect, d)
208    }
209    pub const fn client_write_timeout(d: Duration) -> Self {
210        Self::timeout(TimeoutKind::ClientWrite, d)
211    }
212    pub const fn client_continue_timeout(d: Duration) -> Self {
213        Self::timeout(TimeoutKind::ClientContinue, d)
214    }
215    pub const fn server_header_read_timeout(d: Duration) -> Self {
216        Self::timeout(TimeoutKind::ServerHeaderRead, d)
217    }
218    pub const fn server_body_read_timeout(d: Duration) -> Self {
219        Self::timeout(TimeoutKind::ServerBodyRead, d)
220    }
221    pub const fn server_write_timeout(d: Duration) -> Self {
222        Self::timeout(TimeoutKind::ServerWrite, d)
223    }
224    pub const fn server_idle_timeout(d: Duration) -> Self {
225        Self::timeout(TimeoutKind::ServerIdle, d)
226    }
227}
228
229impl From<io::Error> for Error {
230    fn from(err: io::Error) -> Self {
231        Self::Io(err)
232    }
233}
234
235impl From<Utf8Error> for Error {
236    fn from(e: Utf8Error) -> Self {
237        Self::Protocol(ProtocolError::Utf8(e))
238    }
239}
240
241impl From<FromUtf8Error> for Error {
242    fn from(e: FromUtf8Error) -> Self {
243        Self::Protocol(ProtocolError::FromUtf8(e))
244    }
245}
246
247impl From<InvalidHeaderName> for Error {
248    fn from(e: InvalidHeaderName) -> Self {
249        Self::Protocol(ProtocolError::HeaderName(e))
250    }
251}
252
253impl From<InvalidHeaderValue> for Error {
254    fn from(e: InvalidHeaderValue) -> Self {
255        Self::Protocol(ProtocolError::HeaderValue(e))
256    }
257}
258
259impl From<ToStrError> for Error {
260    fn from(e: ToStrError) -> Self {
261        Self::Protocol(ProtocolError::HeaderToStr(e))
262    }
263}
264
265/// A specific deadline violation. See [`TimeoutKind`] for the list of
266/// recognised deadlines.
267#[derive(Debug, Error)]
268#[error("{kind} timed out after {duration:?}")]
269pub struct TimeoutError {
270    pub kind: TimeoutKind,
271    pub duration: Duration,
272}
273
274/// Identifies which deadline was exceeded.
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276#[non_exhaustive]
277pub enum TimeoutKind {
278    /// Whole client `send` operation.
279    ClientTotal,
280    /// Client TCP connect phase.
281    ClientConnect,
282    /// Client write phase.
283    ClientWrite,
284    /// Client waiting for `100 Continue` from the server.
285    ClientContinue,
286    /// Server reading the ICAP request headers.
287    ServerHeaderRead,
288    /// Server reading the request body.
289    ServerBodyRead,
290    /// Server writing the response.
291    ServerWrite,
292    /// Server idle keep-alive timeout.
293    ServerIdle,
294}
295
296impl fmt::Display for TimeoutKind {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        let s = match self {
299            Self::ClientTotal => "client request",
300            Self::ClientConnect => "client TCP connect",
301            Self::ClientWrite => "client write",
302            Self::ClientContinue => "client 100-continue wait",
303            Self::ServerHeaderRead => "server header read",
304            Self::ServerBodyRead => "server body read",
305            Self::ServerWrite => "server write",
306            Self::ServerIdle => "server idle keep-alive",
307        };
308        f.write_str(s)
309    }
310}
311
312/// Wire-protocol failures: parsing, missing/invalid headers, encapsulation, …
313#[derive(Debug, Error)]
314#[non_exhaustive]
315pub enum ProtocolError {
316    /// Peer closed the connection before a full ICAP header block arrived.
317    #[error("peer closed before ICAP headers")]
318    EarlyClose,
319
320    /// Failed to parse an ICAP message.
321    #[error("ICAP parse error: {0}")]
322    Parse(String),
323
324    /// Failed to parse an embedded HTTP message.
325    #[error("HTTP parse error: {0}")]
326    HttpParse(String),
327
328    /// Required header was absent.
329    #[error("missing required header: {0}")]
330    MissingHeader(&'static str),
331
332    /// Malformed header (catch-all). Prefer a more specific variant when
333    /// possible.
334    #[error("header error: {0}")]
335    Header(String),
336
337    /// Body-handling error (chunked decoder, dechunking, etc.).
338    #[error("body error: {0}")]
339    Body(String),
340
341    /// Embedded body exceeded a configured byte limit.
342    #[error("body too large: {size} bytes (max {max})")]
343    BodyTooLarge { size: usize, max: usize },
344
345    /// Invalid value for a specific ICAP/HTTP field.
346    #[error("invalid {field}: {value}")]
347    InvalidField { field: ProtocolField, value: String },
348
349    /// `ISTag` validation failed.
350    #[error("invalid ISTag: {0}")]
351    InvalidISTag(String),
352
353    /// Outgoing message could not be serialized.
354    #[error("serialization error: {0}")]
355    Serialization(String),
356
357    /// Invalid UTF-8 in wire data.
358    #[error("UTF-8 error: {0}")]
359    Utf8(#[from] Utf8Error),
360
361    /// Invalid UTF-8 while converting owned bytes into text.
362    #[error("UTF-8 conversion error: {0}")]
363    FromUtf8(#[from] FromUtf8Error),
364
365    /// Invalid HTTP header name.
366    #[error("invalid HTTP header name: {0}")]
367    HeaderName(#[from] InvalidHeaderName),
368
369    /// Invalid HTTP header value.
370    #[error("invalid HTTP header value: {0}")]
371    HeaderValue(#[from] InvalidHeaderValue),
372
373    /// HTTP header value could not be represented as text.
374    #[error("invalid HTTP header text: {0}")]
375    HeaderToStr(#[from] ToStrError),
376}
377
378impl ProtocolError {
379    /// Build [`Self::InvalidField`] tersely.
380    pub fn invalid(field: ProtocolField, value: impl Into<String>) -> Self {
381        Self::InvalidField {
382            field,
383            value: value.into(),
384        }
385    }
386}
387
388/// Identifies which ICAP/HTTP field carried an invalid value in
389/// [`ProtocolError::InvalidField`].
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391#[non_exhaustive]
392pub enum ProtocolField {
393    StatusCode,
394    Method,
395    Uri,
396    Version,
397}
398
399impl fmt::Display for ProtocolField {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        let s = match self {
402            Self::StatusCode => "status code",
403            Self::Method => "method",
404            Self::Uri => "URI",
405            Self::Version => "protocol version",
406        };
407        f.write_str(s)
408    }
409}
410
411/// Builder / configuration errors. Surfaced from
412/// [`crate::ServerBuilder::build`] and friends.
413#[derive(Debug, Error)]
414#[non_exhaustive]
415pub enum ConfigError {
416    /// A route was registered without any handlers.
417    #[error("service '{service}' has no handlers")]
418    ServiceWithoutHandlers { service: String },
419
420    /// A service was registered without `ServiceOptions` carrying an `ISTag`.
421    #[error("service '{service}' must configure ServiceOptions with an explicit ISTag")]
422    MissingServiceOptions { service: String },
423
424    /// `ServiceOptions::validate` rejected the configuration.
425    #[error("invalid options for service '{service}': {reason}")]
426    InvalidServiceOptions { service: String, reason: String },
427
428    /// `default_service(...)` points at a name that resolves to no route.
429    #[error("default service '{name}' resolves to unknown service '{resolved}'")]
430    UnknownDefaultService { name: String, resolved: String },
431
432    /// `alias(from, to)` points at a name that resolves to no route.
433    #[error("alias '{from}' resolves to unknown service '{resolved}'")]
434    UnknownAlias { from: String, resolved: String },
435
436    /// Catch-all for anything that does not yet have a structured variant.
437    #[error("{0}")]
438    Other(String),
439}
440
441#[derive(Debug)]
442struct MessageError(String);
443
444impl fmt::Display for MessageError {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        f.write_str(&self.0)
447    }
448}
449
450impl StdError for MessageError {}
451
452/// Convenient alias for results in the ICAP library.
453pub type IcapResult<T> = Result<T, Error>;
454
455/// Converts a generic `Result<T, E>` into an [`IcapResult<T>`] by wrapping
456/// the error in [`Error::External`]. The original error is preserved via
457/// `std::error::Error::source`.
458pub trait ToIcapResult<T> {
459    fn to_icap_result(self) -> IcapResult<T>;
460}
461
462impl<T, E> ToIcapResult<T> for Result<T, E>
463where
464    E: StdError + Send + Sync + 'static,
465{
466    fn to_icap_result(self) -> IcapResult<T> {
467        self.map_err(Error::external)
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn header_value_conversion_routes_through_protocol() {
477        let err: Error = http::HeaderValue::from_str("bad\r\nvalue")
478            .expect_err("header value must be rejected")
479            .into();
480        assert!(matches!(
481            err,
482            Error::Protocol(ProtocolError::HeaderValue(_))
483        ));
484        assert!(StdError::source(&err).is_some());
485    }
486
487    #[test]
488    fn http_builder_conversion_preserves_source_error() {
489        let err: Error = http::Request::builder()
490            .method("bad method")
491            .body(())
492            .expect_err("invalid method must be rejected")
493            .into();
494        assert!(matches!(err, Error::Http(_)));
495        assert!(StdError::source(&err).is_some());
496    }
497
498    #[test]
499    fn to_icap_result_wraps_in_external() {
500        let result: Result<(), io::Error> = Err(io::Error::other("external"));
501        let err = result.to_icap_result().expect_err("external error");
502        assert!(matches!(err, Error::External(_)));
503        assert!(StdError::source(&err).is_some());
504    }
505
506    #[test]
507    fn timeout_helpers_set_kind() {
508        let err = Error::server_write_timeout(Duration::from_millis(5));
509        match err {
510            Error::Timeout(t) => assert_eq!(t.kind, TimeoutKind::ServerWrite),
511            _ => panic!("expected Timeout variant"),
512        }
513    }
514
515    #[test]
516    fn classifiers_agree_with_variant() {
517        let t = Error::client_connect_timeout(Duration::from_millis(1));
518        assert!(t.is_timeout());
519        assert!(t.is_retryable());
520
521        let p = Error::parse("bad");
522        assert!(p.is_protocol());
523        assert!(!p.is_retryable());
524
525        let e = Error::Protocol(ProtocolError::EarlyClose);
526        assert!(e.is_early_close());
527        assert!(e.is_retryable());
528    }
529}