Skip to main content

icap_rs/server/
handler.rs

1//! Error type returned from user-defined route handlers.
2//!
3//! The server converts a [`HandlerError`] into an ICAP response — by default
4//! `500 Internal Server Error` — and logs the underlying source at `WARN`
5//! level. The connection stays open and is ready to serve the next request.
6//!
7//! `HandlerError` is intentionally not a `std::error::Error`: that lets it
8//! accept *any* `std::error::Error + Send + Sync + 'static` through a blanket
9//! `From` impl, so handlers can use `?` to propagate `crate::Error`,
10//! `std::io::Error`, or any user-defined error without manual mapping.
11//!
12//! # Example
13//!
14//! ```
15//! use icap_rs::{HandlerError, HandlerResult, IncomingRequest, Response, StatusCode};
16//!
17//! async fn handler(_req: IncomingRequest) -> HandlerResult<Response> {
18//!     // Propagate a typed error from any layer — turns into 500 automatically.
19//!     let resp = Response::no_content_with_istag("svc-1.0")?;
20//!     Ok(resp)
21//! }
22//!
23//! async fn handler_bad_request(_req: IncomingRequest) -> HandlerResult<Response> {
24//!     Err(HandlerError::new(StatusCode::BAD_REQUEST).with_message("missing URL"))
25//! }
26//! ```
27
28use std::fmt;
29
30use crate::{Response, StatusCode};
31
32/// Boxed `std::error::Error` carried as the source of a [`HandlerError`].
33pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
34
35/// Error returned from a route handler.
36///
37/// See the [module-level docs][self] for the full design rationale.
38#[must_use]
39pub struct HandlerError {
40    status: StatusCode,
41    message: Option<String>,
42    source: Option<BoxError>,
43}
44
45impl HandlerError {
46    /// Create a `HandlerError` with the given status and no source/message.
47    pub fn new(status: StatusCode) -> Self {
48        Self {
49            status,
50            message: None,
51            source: None,
52        }
53    }
54
55    /// Create a 500 Internal Server Error with a human-readable message.
56    pub fn internal(message: impl Into<String>) -> Self {
57        Self {
58            status: StatusCode::INTERNAL_SERVER_ERROR,
59            message: Some(message.into()),
60            source: None,
61        }
62    }
63
64    /// Override the ICAP status emitted to the client.
65    pub const fn with_status(mut self, status: StatusCode) -> Self {
66        self.status = status;
67        self
68    }
69
70    /// Attach a human-readable message used as the response reason phrase.
71    pub fn with_message(mut self, message: impl Into<String>) -> Self {
72        self.message = Some(message.into());
73        self
74    }
75
76    /// Attach an underlying error as the source (preserved in the error chain
77    /// for logging).
78    pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
79        self.source = Some(source.into());
80        self
81    }
82
83    /// The status code the server will send.
84    pub const fn status(&self) -> StatusCode {
85        self.status
86    }
87
88    /// The configured message, if any.
89    pub fn message(&self) -> Option<&str> {
90        self.message.as_deref()
91    }
92
93    /// The underlying source error, if any.
94    pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
95        self.source
96            .as_deref()
97            .map(|e| e as &(dyn std::error::Error + 'static))
98    }
99
100    /// Render the error as an ICAP `Response`.
101    ///
102    /// Uses [`Self::message`] as the reason phrase when set, otherwise the
103    /// canonical reason for the status code, otherwise `"Handler error"`.
104    pub(crate) fn into_response(self) -> Response {
105        let reason = self
106            .message
107            .as_deref()
108            .or_else(|| self.status.canonical_reason())
109            .unwrap_or("Handler error");
110        Response::new(self.status, reason)
111    }
112}
113
114impl fmt::Debug for HandlerError {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.debug_struct("HandlerError")
117            .field("status", &self.status.as_u16())
118            .field("message", &self.message)
119            .field("source", &self.source)
120            .finish()
121    }
122}
123
124impl fmt::Display for HandlerError {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "handler error ({})", self.status.as_u16())?;
127        if let Some(msg) = &self.message {
128            write!(f, ": {msg}")?;
129        }
130        if let Some(src) = &self.source {
131            write!(f, ": {src}")?;
132        }
133        Ok(())
134    }
135}
136
137// Anyhow-style blanket conversion. Works because `HandlerError` itself is
138// intentionally NOT a `std::error::Error`, so no reflexive collision arises.
139impl<E> From<E> for HandlerError
140where
141    E: std::error::Error + Send + Sync + 'static,
142{
143    fn from(err: E) -> Self {
144        Self {
145            status: StatusCode::INTERNAL_SERVER_ERROR,
146            message: None,
147            source: Some(Box::new(err)),
148        }
149    }
150}
151
152/// Convenient alias for results returned from route handlers.
153pub type HandlerResult<T> = Result<T, HandlerError>;