Skip to main content

http_handle/
error.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2026 Sebastien Rousseau
3
4// src/error.rs
5
6//! Error model for runtime, parsing, and policy operations.
7//!
8//! Use [`ServerError`] as the shared error boundary across sync and async server paths.
9
10use std::io;
11use thiserror::Error;
12
13/// Represents the different types of errors that can occur in the server.
14///
15/// This enum defines various errors that can be encountered during the server's operation,
16/// such as I/O errors, invalid requests, file not found, and forbidden access.
17///
18/// # Examples
19///
20/// Creating an I/O error:
21///
22/// ```
23/// use std::io::{Error, ErrorKind};
24/// use http_handle::ServerError;
25///
26/// let io_error = Error::new(ErrorKind::NotFound, "file not found");
27/// let server_error = ServerError::from(io_error);
28/// assert!(matches!(server_error, ServerError::Io(_)));
29/// ```
30///
31/// Creating an invalid request error:
32///
33/// ```
34/// use http_handle::ServerError;
35///
36/// let invalid_request = ServerError::InvalidRequest("Missing HTTP method".to_string());
37/// assert!(matches!(invalid_request, ServerError::InvalidRequest(_)));
38/// ```
39#[derive(Error, Debug)]
40pub enum ServerError {
41    /// An I/O error occurred.
42    #[error("I/O error: {0}")]
43    Io(#[from] io::Error),
44
45    /// The request received by the server was invalid or malformed.
46    #[error("Invalid request: {0}")]
47    InvalidRequest(String),
48
49    /// The requested file was not found on the server.
50    #[error("File not found: {0}")]
51    NotFound(String),
52
53    /// Access to the requested resource is forbidden.
54    #[error("Forbidden: {0}")]
55    Forbidden(String),
56
57    /// A custom error type for unexpected scenarios.
58    #[error("Custom error: {0}")]
59    Custom(String),
60
61    /// A task execution failed (join failure or panic boundary).
62    #[error("Task failed: {0}")]
63    TaskFailed(String),
64}
65
66impl ServerError {
67    /// Creates a new `InvalidRequest` error with the given message.
68    ///
69    /// # Arguments
70    ///
71    /// * `message` - A string slice that holds the error message.
72    ///
73    /// # Returns
74    ///
75    /// A `ServerError::InvalidRequest` variant.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use http_handle::ServerError;
81    ///
82    /// let error = ServerError::invalid_request("Missing HTTP version");
83    /// assert!(matches!(error, ServerError::InvalidRequest(_)));
84    /// ```
85    pub fn invalid_request<T: Into<String>>(message: T) -> Self {
86        ServerError::InvalidRequest(message.into())
87    }
88
89    /// Creates a new `NotFound` error with the given path.
90    ///
91    /// # Arguments
92    ///
93    /// * `path` - A string slice that holds the path of the not found resource.
94    ///
95    /// # Returns
96    ///
97    /// A `ServerError::NotFound` variant.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use http_handle::ServerError;
103    ///
104    /// let error = ServerError::not_found("/nonexistent.html");
105    /// assert!(matches!(error, ServerError::NotFound(_)));
106    /// ```
107    pub fn not_found<T: Into<String>>(path: T) -> Self {
108        ServerError::NotFound(path.into())
109    }
110
111    /// Creates a new `Forbidden` error with the given message.
112    ///
113    /// # Arguments
114    ///
115    /// * `message` - A string slice that holds the error message.
116    ///
117    /// # Returns
118    ///
119    /// A `ServerError::Forbidden` variant.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use http_handle::ServerError;
125    ///
126    /// let error = ServerError::forbidden("Access denied to sensitive file");
127    /// assert!(matches!(error, ServerError::Forbidden(_)));
128    /// ```
129    pub fn forbidden<T: Into<String>>(message: T) -> Self {
130        ServerError::Forbidden(message.into())
131    }
132}
133
134impl From<&str> for ServerError {
135    /// Converts a string slice into a `ServerError::Custom` variant.
136    ///
137    /// This implementation allows for easy creation of custom errors from string literals.
138    ///
139    /// # Arguments
140    ///
141    /// * `error` - A string slice that holds the error message.
142    ///
143    /// # Returns
144    ///
145    /// A `ServerError::Custom` variant.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use http_handle::ServerError;
151    ///
152    /// let error: ServerError = "Unexpected error".into();
153    /// assert!(matches!(error, ServerError::Custom(_)));
154    /// ```
155    fn from(error: &str) -> Self {
156        ServerError::Custom(error.to_string())
157    }
158}
159
160impl From<ServerError> for io::Error {
161    /// Converts a `ServerError` into an `io::Error`.
162    ///
163    /// This implementation enables the `?` operator to convert `ServerError`
164    /// to `io::Error` when needed, particularly for functions that return
165    /// `io::Result<()>` but work with `ServerError` internally.
166    ///
167    /// # Arguments
168    ///
169    /// * `error` - A `ServerError` to convert.
170    ///
171    /// # Returns
172    ///
173    /// An `io::Error` with the appropriate error kind and message.
174    fn from(error: ServerError) -> Self {
175        match error {
176            ServerError::Io(io_error) => io_error,
177            ServerError::InvalidRequest(msg) => {
178                io::Error::new(io::ErrorKind::InvalidInput, msg)
179            }
180            ServerError::NotFound(msg) => {
181                io::Error::new(io::ErrorKind::NotFound, msg)
182            }
183            ServerError::Forbidden(msg) => {
184                io::Error::new(io::ErrorKind::PermissionDenied, msg)
185            }
186            ServerError::Custom(msg) => io::Error::other(msg),
187            ServerError::TaskFailed(msg) => io::Error::other(msg),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::io;
196
197    /// Test case for converting an `io::Error` into `ServerError::Io`.
198    #[test]
199    fn test_io_error_conversion() {
200        let io_error =
201            io::Error::new(io::ErrorKind::NotFound, "file not found");
202        let server_error = ServerError::from(io_error);
203        assert!(matches!(server_error, ServerError::Io(_)));
204    }
205
206    /// Test case for creating a `ServerError::Custom` from a string slice.
207    #[test]
208    fn test_custom_error_creation() {
209        let custom_error: ServerError = "Unexpected error".into();
210        assert!(matches!(custom_error, ServerError::Custom(_)));
211    }
212
213    /// Test case for verifying the error messages of different `ServerError` variants.
214    #[test]
215    fn test_error_messages() {
216        let not_found = ServerError::not_found("index.html");
217        assert_eq!(not_found.to_string(), "File not found: index.html");
218
219        let forbidden = ServerError::forbidden("Access denied");
220        assert_eq!(forbidden.to_string(), "Forbidden: Access denied");
221
222        let invalid_request =
223            ServerError::invalid_request("Missing HTTP method");
224        assert_eq!(
225            invalid_request.to_string(),
226            "Invalid request: Missing HTTP method"
227        );
228    }
229
230    /// Test case for creating a `ServerError::InvalidRequest` using the `invalid_request` method.
231    #[test]
232    fn test_invalid_request_creation() {
233        let invalid_request =
234            ServerError::invalid_request("Bad request");
235        assert!(matches!(
236            invalid_request,
237            ServerError::InvalidRequest(_)
238        ));
239        assert_eq!(
240            invalid_request.to_string(),
241            "Invalid request: Bad request"
242        );
243    }
244
245    /// Test case for creating a `ServerError::NotFound` using the `not_found` method.
246    #[test]
247    fn test_not_found_creation() {
248        let not_found = ServerError::not_found("/nonexistent.html");
249        assert!(matches!(not_found, ServerError::NotFound(_)));
250        assert_eq!(
251            not_found.to_string(),
252            "File not found: /nonexistent.html"
253        );
254    }
255
256    /// Test case for creating a `ServerError::Forbidden` using the `forbidden` method.
257    #[test]
258    fn test_forbidden_creation() {
259        let forbidden = ServerError::forbidden("Access denied");
260        assert!(matches!(forbidden, ServerError::Forbidden(_)));
261        assert_eq!(forbidden.to_string(), "Forbidden: Access denied");
262    }
263
264    /// Test case for verifying the `ServerError::Custom` variant and its error message.
265    #[test]
266    fn test_custom_error_message() {
267        let custom_error =
268            ServerError::Custom("Custom error occurred".to_string());
269        assert_eq!(
270            custom_error.to_string(),
271            "Custom error: Custom error occurred"
272        );
273    }
274
275    /// Test case for checking `ServerError::from` for string conversion.
276    #[test]
277    fn test_custom_error_from_str() {
278        let custom_error: ServerError = "Some custom error".into();
279        assert!(matches!(custom_error, ServerError::Custom(_)));
280        assert_eq!(
281            custom_error.to_string(),
282            "Custom error: Some custom error"
283        );
284    }
285
286    #[test]
287    fn test_task_failed_error_message() {
288        let task_failed =
289            ServerError::TaskFailed("panic in task".to_string());
290        assert_eq!(
291            task_failed.to_string(),
292            "Task failed: panic in task"
293        );
294    }
295
296    /// Test case for converting `io::Error` using a different error kind to `ServerError::Io`.
297    #[test]
298    fn test_io_error_conversion_other_kind() {
299        let io_error = io::Error::new(
300            io::ErrorKind::PermissionDenied,
301            "permission denied",
302        );
303        let server_error = ServerError::from(io_error);
304        assert!(matches!(server_error, ServerError::Io(_)));
305        assert_eq!(
306            server_error.to_string(),
307            "I/O error: permission denied"
308        );
309    }
310
311    /// Test case for verifying if `ServerError::InvalidRequest` carries the correct error message.
312    #[test]
313    fn test_invalid_request_message() {
314        let error_message = "Invalid HTTP version";
315        let invalid_request =
316            ServerError::InvalidRequest(error_message.to_string());
317        assert_eq!(
318            invalid_request.to_string(),
319            format!("Invalid request: {}", error_message)
320        );
321    }
322
323    /// Test case for verifying if `ServerError::NotFound` carries the correct file path.
324    #[test]
325    fn test_not_found_message() {
326        let file_path = "missing.html";
327        let not_found = ServerError::NotFound(file_path.to_string());
328        assert_eq!(
329            not_found.to_string(),
330            format!("File not found: {}", file_path)
331        );
332    }
333
334    /// Test case for verifying if `ServerError::Forbidden` carries the correct message.
335    #[test]
336    fn test_forbidden_message() {
337        let forbidden_message = "Access denied to private resource";
338        let forbidden =
339            ServerError::Forbidden(forbidden_message.to_string());
340        assert_eq!(
341            forbidden.to_string(),
342            format!("Forbidden: {}", forbidden_message)
343        );
344    }
345
346    /// Test case for `ServerError::Io` with a generic IO error to ensure correct propagation.
347    #[test]
348    fn test_io_error_generic() {
349        let io_error = io::Error::other("generic I/O error");
350        let server_error = ServerError::from(io_error);
351        assert!(matches!(server_error, ServerError::Io(_)));
352        assert_eq!(
353            server_error.to_string(),
354            "I/O error: generic I/O error"
355        );
356    }
357
358    #[test]
359    fn test_server_error_to_io_error_conversion() {
360        let converted: io::Error =
361            ServerError::invalid_request("invalid").into();
362        assert_eq!(converted.kind(), io::ErrorKind::InvalidInput);
363
364        let converted: io::Error = ServerError::not_found("x").into();
365        assert_eq!(converted.kind(), io::ErrorKind::NotFound);
366
367        let converted: io::Error = ServerError::forbidden("x").into();
368        assert_eq!(converted.kind(), io::ErrorKind::PermissionDenied);
369
370        let converted: io::Error =
371            ServerError::Custom("custom".to_string()).into();
372        assert_eq!(converted.kind(), io::ErrorKind::Other);
373
374        let converted: io::Error =
375            ServerError::TaskFailed("task".to_string()).into();
376        assert_eq!(converted.kind(), io::ErrorKind::Other);
377
378        let source = io::Error::new(io::ErrorKind::TimedOut, "timeout");
379        let converted: io::Error = ServerError::Io(source).into();
380        assert_eq!(converted.kind(), io::ErrorKind::TimedOut);
381    }
382}