Skip to main content

reinhardt_http/
exception.rs

1//! Exception handling extension point for dispatch errors.
2//!
3//! This module provides the [`ExceptionHandler`] hook and
4//! [`ExceptionHandlingHandler`], the adapter that applies it to a handler chain.
5//!
6//! Without an installed handler every error is converted by
7//! `impl From<Error> for Response`, which omits internal details and emits a
8//! JSON `SafeErrorResponse` with a safe category message and, for applicable
9//! client errors, a controlled detail. Installing a handler replaces that
10//! conversion so an application can present a fixed error shape to its clients.
11
12use async_trait::async_trait;
13use std::sync::Arc;
14
15use crate::{Error, Handler, Request, Response, Result};
16
17/// A strategy for turning a dispatch error into an HTTP response.
18///
19/// Install an implementation with `with_exception_handler` on the router, the
20/// server, or a middleware chain. Every error the framework produces while
21/// serving a request then flows through this method instead of the default
22/// `impl From<Error> for Response` conversion.
23///
24/// # Responsibility
25///
26/// Installing a handler transfers responsibility for the response body and
27/// headers to the handler. The default conversion never exposes internal
28/// details and returns JSON with `Content-Type: application/json`; server
29/// errors use a generic category and client errors may include a safe detail.
30/// A custom handler owns the response headers and provides none of these
31/// guarantees unless it sets them itself. Interpolating `Display` output of
32/// the error into a response body can disclose internal paths and credentials.
33///
34/// # Panics
35///
36/// A panicking handler is not caught here. The request's connection task fails
37/// and the server process stays alive.
38///
39/// # Examples
40///
41/// ```
42/// use async_trait::async_trait;
43/// use hyper::StatusCode;
44/// use reinhardt_http::{Error, ExceptionHandler, Request, Response};
45///
46/// struct JsonErrors;
47///
48/// #[async_trait]
49/// impl ExceptionHandler for JsonErrors {
50///     async fn handle_exception(&self, _request: &Request, error: Error) -> Response {
51///         let status = StatusCode::from_u16(error.status_code())
52///             .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
53///         Response::new(status).with_body("error")
54///     }
55/// }
56/// ```
57#[async_trait]
58pub trait ExceptionHandler: Send + Sync + 'static {
59	/// Builds the response for `error`.
60	///
61	/// `request` carries the method, URI, version, headers, path parameters,
62	/// query parameters and extensions of the original request, which is
63	/// exactly what [`Request::clone_for_di`] preserves. Its body is empty
64	/// because the original request body has already been consumed by the
65	/// handler that produced the error.
66	async fn handle_exception(&self, request: &Request, error: Error) -> Response;
67}
68
69/// Records that an installed [`ExceptionHandler`] has handled an error.
70#[doc(hidden)]
71#[derive(Debug, Clone, Copy)]
72pub struct ExceptionHandlerInvoked;
73
74/// Applies an [`ExceptionHandler`] to the errors produced by an inner handler.
75///
76/// Wrap the innermost handler of a chain with this adapter to route its errors
77/// through the installed handler. The adapter always yields `Ok`, so an outer
78/// wrapper that converts errors cannot observe them.
79///
80/// This adapter sees only errors that reach it as `Err`. Errors raised by
81/// middleware inside a [`MiddlewareChain`](crate::MiddlewareChain) are converted
82/// by that chain. The adapter propagates its handler through request extensions so
83/// nested chains inherit it unless they have an explicitly installed handler.
84///
85/// # Examples
86///
87/// ```
88/// use async_trait::async_trait;
89/// use bytes::Bytes;
90/// use hyper::{HeaderMap, Method, StatusCode, Version};
91/// use reinhardt_http::{
92///     Error, ExceptionHandler, ExceptionHandlingHandler, Handler, Request, Response,
93/// };
94/// use std::sync::Arc;
95///
96/// struct FailingHandler;
97///
98/// #[async_trait]
99/// impl Handler for FailingHandler {
100///     async fn handle(&self, _request: Request) -> reinhardt_http::Result<Response> {
101///         Err(Error::NotFound("no route".to_string()))
102///     }
103/// }
104///
105/// struct TeapotErrors;
106///
107/// #[async_trait]
108/// impl ExceptionHandler for TeapotErrors {
109///     async fn handle_exception(&self, _request: &Request, _error: Error) -> Response {
110///         Response::new(StatusCode::IM_A_TEAPOT)
111///     }
112/// }
113///
114/// # #[tokio::main]
115/// # async fn main() {
116/// let handler = ExceptionHandlingHandler::new(
117///     Arc::new(FailingHandler),
118///     Arc::new(TeapotErrors),
119/// );
120///
121/// let request = Request::builder()
122///     .method(Method::GET)
123///     .uri("/")
124///     .version(Version::HTTP_11)
125///     .headers(HeaderMap::new())
126///     .body(Bytes::new())
127///     .build()
128///     .unwrap();
129///
130/// let response = handler.handle(request).await.unwrap();
131/// assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
132/// # }
133/// ```
134pub struct ExceptionHandlingHandler {
135	inner: Arc<dyn Handler>,
136	exception_handler: Arc<dyn ExceptionHandler>,
137}
138
139impl ExceptionHandlingHandler {
140	/// Creates an adapter that converts `inner`'s errors with `exception_handler`.
141	pub fn new(inner: Arc<dyn Handler>, exception_handler: Arc<dyn ExceptionHandler>) -> Self {
142		Self {
143			inner,
144			exception_handler,
145		}
146	}
147}
148
149#[async_trait]
150impl Handler for ExceptionHandlingHandler {
151	async fn handle(&self, mut request: Request) -> Result<Response> {
152		// `Request` is not `Clone` because it owns parsed-body state. Capture the
153		// context with `clone_for_di`, which copies method, URI, version, headers,
154		// path parameters and query parameters, and shares the extensions store
155		// (auth state, DI context) through an internal `Arc`. This cost is paid
156		// only where a custom handler is installed, because this adapter is only
157		// constructed in that case.
158		request.install_exception_handler(Arc::clone(&self.exception_handler));
159		let mut context = request.clone_for_di();
160		match self.inner.handle(request).await {
161			Ok(response) => Ok(response),
162			Err(error) => {
163				context.sync_path_params_from_shared_state();
164				context.extensions.insert(ExceptionHandlerInvoked);
165				Ok(self
166					.exception_handler
167					.handle_exception(&context, error)
168					.await)
169			}
170		}
171	}
172}
173
174#[cfg(test)]
175mod tests {
176	use super::*;
177	use bytes::Bytes;
178	use hyper::{HeaderMap, Method, StatusCode, Version};
179	use rstest::rstest;
180	use std::sync::Mutex;
181
182	/// Marker type stored in the request's DI context.
183	#[derive(Debug, PartialEq, Eq)]
184	struct MarkerContext(&'static str);
185
186	/// What an exception handler observed about the request it was given.
187	#[derive(Debug, PartialEq, Eq)]
188	struct ObservedRequest {
189		method: Method,
190		path: String,
191		request_id: Option<String>,
192		item_id: Option<String>,
193		di_marker: Option<&'static str>,
194	}
195
196	/// Exception handler that records the request context it receives.
197	struct ObservingHandler {
198		observed: Arc<Mutex<Vec<ObservedRequest>>>,
199	}
200
201	#[async_trait]
202	impl ExceptionHandler for ObservingHandler {
203		async fn handle_exception(&self, request: &Request, error: Error) -> Response {
204			let di_marker = request
205				.get_di_context::<MarkerContext>()
206				.map(|marker| marker.0);
207			self.observed.lock().unwrap().push(ObservedRequest {
208				method: request.method.clone(),
209				path: request.uri.path().to_string(),
210				request_id: request.get_header("x-request-id"),
211				item_id: request.path_params.get("id").cloned(),
212				di_marker,
213			});
214
215			// Echo the error so the assertion can prove the error travelled through.
216			Response::new(StatusCode::IM_A_TEAPOT).with_body(error.to_string())
217		}
218	}
219
220	/// Handler that always fails with the error produced by `factory`.
221	///
222	/// `Error` is not `Clone`, so the factory builds a fresh value per call.
223	struct FailingHandler {
224		factory: fn() -> Error,
225	}
226
227	#[async_trait]
228	impl Handler for FailingHandler {
229		async fn handle(&self, _request: Request) -> Result<Response> {
230			Err((self.factory)())
231		}
232	}
233
234	/// Handler that always succeeds.
235	struct OkHandler;
236
237	#[async_trait]
238	impl Handler for OkHandler {
239		async fn handle(&self, _request: Request) -> Result<Response> {
240			Ok(Response::ok().with_body("ok"))
241		}
242	}
243
244	/// Handler that counts how many times it ran.
245	struct CountingHandler {
246		calls: Arc<Mutex<usize>>,
247	}
248
249	#[async_trait]
250	impl Handler for CountingHandler {
251		async fn handle(&self, _request: Request) -> Result<Response> {
252			*self.calls.lock().unwrap() += 1;
253			Err(Error::Internal("boom".to_string()))
254		}
255	}
256
257	fn build_request(method: Method, uri: &str) -> Request {
258		Request::builder()
259			.method(method)
260			.uri(uri)
261			.version(Version::HTTP_11)
262			.headers(HeaderMap::new())
263			.body(Bytes::new())
264			.build()
265			.unwrap()
266	}
267
268	#[rstest]
269	#[tokio::test]
270	async fn test_inner_error_is_converted_by_installed_handler() {
271		// Arrange
272		let observed = Arc::new(Mutex::new(Vec::new()));
273		let handler = ExceptionHandlingHandler::new(
274			Arc::new(FailingHandler {
275				factory: || Error::NotFound("no route".to_string()),
276			}),
277			Arc::new(ObservingHandler {
278				observed: Arc::clone(&observed),
279			}),
280		);
281		let request = build_request(Method::GET, "/missing");
282
283		// Act
284		let response = handler.handle(request).await.unwrap();
285
286		// Assert
287		assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
288		assert_eq!(observed.lock().unwrap().len(), 1);
289	}
290
291	#[rstest]
292	#[tokio::test]
293	async fn test_handler_receives_original_request_context() {
294		// Arrange
295		let observed = Arc::new(Mutex::new(Vec::new()));
296		let handler = ExceptionHandlingHandler::new(
297			Arc::new(FailingHandler {
298				factory: || Error::Internal("boom".to_string()),
299			}),
300			Arc::new(ObservingHandler {
301				observed: Arc::clone(&observed),
302			}),
303		);
304
305		let mut headers = HeaderMap::new();
306		headers.insert("x-request-id", "req-42".parse().unwrap());
307		let mut request = Request::builder()
308			.method(Method::POST)
309			.uri("/api/items/7")
310			.version(Version::HTTP_11)
311			.headers(headers)
312			.body(Bytes::from_static(b"payload"))
313			.build()
314			.unwrap();
315		request.path_params.insert("id", "7");
316
317		// Act
318		handler.handle(request).await.unwrap();
319
320		// Assert
321		let observed = observed.lock().unwrap();
322		assert_eq!(observed.len(), 1);
323		assert_eq!(observed[0].method, Method::POST);
324		assert_eq!(observed[0].path, "/api/items/7");
325		assert_eq!(observed[0].request_id, Some("req-42".to_string()));
326		assert_eq!(observed[0].item_id, Some("7".to_string()));
327	}
328
329	#[rstest]
330	#[tokio::test]
331	async fn test_handler_shares_request_extensions() {
332		// Arrange
333		let observed = Arc::new(Mutex::new(Vec::new()));
334		let handler = ExceptionHandlingHandler::new(
335			Arc::new(FailingHandler {
336				factory: || Error::Internal("boom".to_string()),
337			}),
338			Arc::new(ObservingHandler {
339				observed: Arc::clone(&observed),
340			}),
341		);
342		let mut request = build_request(Method::GET, "/");
343		request.set_di_context(MarkerContext("di-visible"));
344
345		// Act
346		handler.handle(request).await.unwrap();
347
348		// Assert: the extensions store is shared with the context request
349		let observed = observed.lock().unwrap();
350		assert_eq!(observed.len(), 1);
351		assert_eq!(observed[0].di_marker, Some("di-visible"));
352	}
353
354	#[rstest]
355	#[tokio::test]
356	async fn test_handler_is_not_invoked_for_successful_response() {
357		// Arrange
358		let observed = Arc::new(Mutex::new(Vec::new()));
359		let handler = ExceptionHandlingHandler::new(
360			Arc::new(OkHandler),
361			Arc::new(ObservingHandler {
362				observed: Arc::clone(&observed),
363			}),
364		);
365
366		// Act
367		let response = handler
368			.handle(build_request(Method::GET, "/"))
369			.await
370			.unwrap();
371
372		// Assert
373		assert_eq!(response.status, StatusCode::OK);
374		assert_eq!(String::from_utf8(response.body.to_vec()).unwrap(), "ok");
375		assert!(observed.lock().unwrap().is_empty());
376	}
377
378	#[rstest]
379	#[tokio::test]
380	async fn test_inner_handler_runs_exactly_once() {
381		// Arrange
382		let calls = Arc::new(Mutex::new(0_usize));
383		let observed = Arc::new(Mutex::new(Vec::new()));
384		let handler = ExceptionHandlingHandler::new(
385			Arc::new(CountingHandler {
386				calls: Arc::clone(&calls),
387			}),
388			Arc::new(ObservingHandler {
389				observed: Arc::clone(&observed),
390			}),
391		);
392
393		// Act
394		handler
395			.handle(build_request(Method::GET, "/"))
396			.await
397			.unwrap();
398
399		// Assert
400		assert_eq!(*calls.lock().unwrap(), 1);
401		assert_eq!(observed.lock().unwrap().len(), 1);
402	}
403}