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}