1use async_trait::async_trait;
7use bytes::Bytes;
8use hyper::StatusCode;
9use reinhardt_http::{ExceptionHandler as HttpExceptionHandler, Request, Response};
10use std::fmt;
11use std::future::Future;
12use std::sync::Arc;
13use tracing::{error, warn};
14
15use crate::DispatchError;
16use crate::build_error_response;
17
18pub type ExceptionResult = Result<Response, DispatchError>;
20
21#[async_trait]
29pub trait ExceptionHandler: Send + Sync {
30 async fn handle_exception(&self, request: &Request, error: DispatchError) -> Response;
32}
33
34pub(crate) fn dispatch_error_to_exception(
37 error: DispatchError,
38) -> reinhardt_core::exception::Error {
39 match error {
40 DispatchError::Middleware(message)
41 | DispatchError::View(message)
42 | DispatchError::Internal(message) => reinhardt_core::exception::Error::Internal(message),
43 DispatchError::UrlResolution(message) => {
44 reinhardt_core::exception::Error::NotFound(message)
45 }
46 DispatchError::Http(message) => reinhardt_core::exception::Error::Http(message),
47 }
48}
49
50pub(crate) fn exception_to_dispatch_error(
53 error: reinhardt_core::exception::Error,
54) -> DispatchError {
55 match error {
56 reinhardt_core::exception::Error::NotFound(message) => {
57 DispatchError::UrlResolution(message)
58 }
59 reinhardt_core::exception::Error::Http(message) => DispatchError::Http(message),
60 error => DispatchError::View(error.to_string()),
61 }
62}
63
64pub fn adapt_exception_handler(
107 handler: Arc<dyn ExceptionHandler>,
108) -> Arc<dyn HttpExceptionHandler> {
109 Arc::new(LegacyExceptionHandlerAdapter { handler })
110}
111
112struct LegacyExceptionHandlerAdapter {
113 handler: Arc<dyn ExceptionHandler>,
114}
115
116#[async_trait]
117impl HttpExceptionHandler for LegacyExceptionHandlerAdapter {
118 async fn handle_exception(
119 &self,
120 request: &Request,
121 error: reinhardt_core::exception::Error,
122 ) -> Response {
123 self.handler
124 .handle_exception(request, exception_to_dispatch_error(error))
125 .await
126 }
127}
128
129pub struct DefaultExceptionHandler;
133
134#[async_trait]
135impl HttpExceptionHandler for DefaultExceptionHandler {
136 async fn handle_exception(
137 &self,
138 _request: &Request,
139 error: reinhardt_core::exception::Error,
140 ) -> Response {
141 if error.status_code() >= 500 {
144 error!("Dispatch error: {}", error);
145 } else {
146 warn!("Dispatch error: {}", error);
147 }
148 let status =
149 StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
150 let client_message = match status {
151 StatusCode::BAD_REQUEST => "Bad Request",
152 StatusCode::UNAUTHORIZED => "Unauthorized",
153 StatusCode::FORBIDDEN => "Forbidden",
154 StatusCode::NOT_FOUND => "Not Found",
155 StatusCode::METHOD_NOT_ALLOWED => "Method Not Allowed",
156 StatusCode::CONFLICT => "Conflict",
157 _ => "Internal Server Error",
158 };
159
160 build_error_response(status, client_message)
161 }
162}
163
164#[async_trait]
165impl ExceptionHandler for DefaultExceptionHandler {
166 async fn handle_exception(&self, request: &Request, error: DispatchError) -> Response {
167 HttpExceptionHandler::handle_exception(self, request, dispatch_error_to_exception(error))
168 .await
169 }
170}
171
172pub async fn convert_exception_to_response<F, Fut>(handler: F, request: Request) -> Response
182where
183 F: FnOnce(Request) -> Fut,
184 Fut: Future<Output = Result<Response, DispatchError>>,
185{
186 let method = request.method.clone();
189 let uri = request.uri.clone();
190 let version = request.version;
191 let headers = request.headers.clone();
192
193 match handler(request).await {
194 Ok(response) => response,
195 Err(error) => {
196 let exception_handler = DefaultExceptionHandler;
197 match Request::builder()
199 .method(method)
200 .uri(uri.to_string())
201 .version(version)
202 .headers(headers)
203 .body(Bytes::new())
204 .build()
205 {
206 Ok(context_request) => {
207 HttpExceptionHandler::handle_exception(
208 &exception_handler,
209 &context_request,
210 dispatch_error_to_exception(error),
211 )
212 .await
213 }
214 Err(_) => {
215 let mut response = Response::new(hyper::StatusCode::INTERNAL_SERVER_ERROR);
216 response.body = Bytes::from("Internal Server Error");
217 response
218 }
219 }
220 }
221 }
222}
223
224pub trait IntoResponse {
226 fn into_response(self) -> Response;
228}
229
230impl IntoResponse for Response {
231 fn into_response(self) -> Response {
232 self
233 }
234}
235
236impl IntoResponse for String {
237 fn into_response(self) -> Response {
238 let mut response = Response::new(StatusCode::OK);
239 response.body = Bytes::from(self.into_bytes());
240 response
241 }
242}
243
244impl IntoResponse for &str {
245 fn into_response(self) -> Response {
246 let mut response = Response::new(StatusCode::OK);
247 response.body = Bytes::from(self.as_bytes().to_vec());
248 response
249 }
250}
251
252impl IntoResponse for Vec<u8> {
253 fn into_response(self) -> Response {
254 let mut response = Response::new(StatusCode::OK);
255 response.body = Bytes::from(self);
256 response
257 }
258}
259
260impl IntoResponse for StatusCode {
261 fn into_response(self) -> Response {
262 Response::new(self)
263 }
264}
265
266impl<T: IntoResponse, E: fmt::Display> IntoResponse for Result<T, E> {
267 fn into_response(self) -> Response {
268 match self {
269 Ok(value) => value.into_response(),
270 Err(error) => {
271 error!("Error converting to response: {}", error);
273 build_error_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
274 }
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use reinhardt_http::ExceptionHandler as HttpExceptionHandler;
283 use rstest::rstest;
284 use std::sync::Arc;
285
286 fn build_request() -> Request {
287 Request::builder()
288 .method(hyper::Method::GET)
289 .uri("/")
290 .version(hyper::Version::HTTP_11)
291 .headers(hyper::HeaderMap::new())
292 .body(Bytes::new())
293 .build()
294 .unwrap()
295 }
296
297 #[tokio::test]
302 async fn test_internal_error_does_not_expose_details() {
303 let handler = DefaultExceptionHandler;
305 let request = build_request();
306 let error = dispatch_error_to_exception(DispatchError::Internal(
307 "database pool exhausted at /src/db/pool.rs:99".to_string(),
308 ));
309
310 let response = HttpExceptionHandler::handle_exception(&handler, &request, error).await;
312
313 let body = String::from_utf8(response.body.to_vec()).unwrap();
315 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
316 assert_eq!(body, "Internal Server Error");
317 assert!(!body.contains("database"));
318 assert!(!body.contains(".rs:"));
319 }
320
321 #[tokio::test]
322 async fn test_middleware_error_does_not_expose_details() {
323 let handler = DefaultExceptionHandler;
325 let request = build_request();
326 let error = dispatch_error_to_exception(DispatchError::Middleware(
327 "JWT decode failed: invalid signature for key abc123".to_string(),
328 ));
329
330 let response = HttpExceptionHandler::handle_exception(&handler, &request, error).await;
332
333 let body = String::from_utf8(response.body.to_vec()).unwrap();
335 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
336 assert_eq!(body, "Internal Server Error");
337 assert!(!body.contains("JWT"));
338 assert!(!body.contains("abc123"));
339 }
340
341 #[tokio::test]
342 async fn test_view_error_does_not_expose_details() {
343 let handler = DefaultExceptionHandler;
345 let request = build_request();
346 let error = dispatch_error_to_exception(DispatchError::View(
347 "template rendering panicked at /src/views/admin.rs:42".to_string(),
348 ));
349
350 let response = HttpExceptionHandler::handle_exception(&handler, &request, error).await;
352
353 let body = String::from_utf8(response.body.to_vec()).unwrap();
355 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
356 assert_eq!(body, "Internal Server Error");
357 assert!(!body.contains("panicked"));
358 assert!(!body.contains(".rs:"));
359 }
360
361 #[tokio::test]
362 async fn test_url_resolution_returns_not_found() {
363 let handler = DefaultExceptionHandler;
365 let request = build_request();
366 let error = dispatch_error_to_exception(DispatchError::UrlResolution(
367 "no route matched".to_string(),
368 ));
369
370 let response = HttpExceptionHandler::handle_exception(&handler, &request, error).await;
372
373 let body = String::from_utf8(response.body.to_vec()).unwrap();
375 assert_eq!(response.status, StatusCode::NOT_FOUND);
376 assert_eq!(body, "Not Found");
377 }
378
379 #[tokio::test]
380 async fn test_http_error_returns_bad_request() {
381 let handler = DefaultExceptionHandler;
383 let request = build_request();
384 let error =
385 dispatch_error_to_exception(DispatchError::Http("malformed header".to_string()));
386
387 let response = HttpExceptionHandler::handle_exception(&handler, &request, error).await;
389
390 let body = String::from_utf8(response.body.to_vec()).unwrap();
392 assert_eq!(response.status, StatusCode::BAD_REQUEST);
393 assert_eq!(body, "Bad Request");
394 }
395
396 #[rstest]
397 #[tokio::test]
398 async fn legacy_exception_handler_can_be_adapted_to_http_hook() {
399 struct LegacyTeapot;
401
402 #[async_trait]
403 impl ExceptionHandler for LegacyTeapot {
404 async fn handle_exception(&self, _request: &Request, error: DispatchError) -> Response {
405 assert!(matches!(error, DispatchError::UrlResolution(_)));
406 Response::new(StatusCode::IM_A_TEAPOT)
407 }
408 }
409
410 let request = build_request();
411 let handler = adapt_exception_handler(Arc::new(LegacyTeapot));
412
413 let response = HttpExceptionHandler::handle_exception(
415 handler.as_ref(),
416 &request,
417 reinhardt_core::exception::Error::NotFound("missing".to_owned()),
418 )
419 .await;
420
421 assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
423 }
424
425 #[rstest]
426 #[tokio::test]
427 async fn legacy_exception_handler_preserves_http_error_category() {
428 struct LegacyHttp;
430
431 #[async_trait]
432 impl ExceptionHandler for LegacyHttp {
433 async fn handle_exception(&self, _request: &Request, error: DispatchError) -> Response {
434 assert!(matches!(error, DispatchError::Http(_)));
435 Response::new(StatusCode::IM_A_TEAPOT)
436 }
437 }
438
439 let request = build_request();
440 let handler = adapt_exception_handler(Arc::new(LegacyHttp));
441
442 let response = HttpExceptionHandler::handle_exception(
444 handler.as_ref(),
445 &request,
446 reinhardt_core::exception::Error::Http("malformed header".to_owned()),
447 )
448 .await;
449
450 assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
452 }
453
454 #[test]
455 fn test_into_response_for_result_err_does_not_expose_error() {
456 let result: Result<String, String> =
458 Err("connection string: postgres://admin:pass@host/db".to_string());
459
460 let response = result.into_response();
462
463 let body = String::from_utf8(response.body.to_vec()).unwrap();
465 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
466 assert!(!body.contains("postgres"));
467 assert!(!body.contains("admin"));
468 assert_eq!(body, "Internal Server Error");
469 }
470}