use hyper::StatusCode;
use reinhardt_core::signals::{
RequestFinishedEvent, RequestStartedEvent, request_finished, request_started,
};
use reinhardt_http::Handler;
use reinhardt_http::{Request, Response};
use reinhardt_urls::routers::DefaultRouter;
use std::sync::Arc;
use tracing::{debug, error, trace, warn};
use crate::{DispatchError, exception::exception_to_dispatch_error};
pub struct BaseHandler {
#[allow(dead_code)]
is_async: bool,
router: Option<Arc<DefaultRouter>>,
}
impl BaseHandler {
pub fn new() -> Self {
Self {
is_async: true,
router: None,
}
}
pub fn with_router(router: Arc<DefaultRouter>) -> Self {
Self {
is_async: true,
router: Some(router),
}
}
pub async fn handle_request(
&self,
request: Request,
) -> std::result::Result<Response, DispatchError> {
match self.handle_request_with_errors(request).await {
Err(DispatchError::UrlResolution(_)) => Ok(Response::new(StatusCode::NOT_FOUND)),
response => response,
}
}
async fn handle_request_with_errors(
&self,
request: Request,
) -> std::result::Result<Response, DispatchError> {
self.handle_request_with_framework_errors(request)
.await
.map_err(exception_to_dispatch_error)
}
async fn handle_request_with_framework_errors(
&self,
request: Request,
) -> reinhardt_core::exception::Result<Response> {
trace!("Handling request: {:?}", request.uri);
let event = RequestStartedEvent::new();
if let Err(e) = request_started().send(event).await {
warn!("Failed to send request_started signal: {}", e);
}
let response = Self::get_response_async(request, self.router.as_ref()).await;
let event = RequestFinishedEvent::new();
if let Err(e) = request_finished().send(event).await {
warn!("Failed to send request_finished signal: {}", e);
}
response
}
async fn get_response_async(
request: Request,
router: Option<&Arc<DefaultRouter>>,
) -> reinhardt_core::exception::Result<Response> {
debug!("Getting response for: {}", request.uri.path());
if let Some(router) = router {
trace!("Attempting to route request through router");
match router.handle(request).await {
Ok(response) => {
trace!("Route handled successfully");
return Ok(response);
}
Err(reinhardt_core::exception::Error::NotFound(msg)) => {
debug!("No route matched: {}", msg);
return Err(reinhardt_core::exception::Error::NotFound(msg));
}
Err(e) => {
error!("Handler error: {}", e);
return Err(e);
}
}
}
debug!("No router configured, returning a URL resolution error");
Err(reinhardt_core::exception::Error::NotFound(
"No router configured".to_owned(),
))
}
pub async fn handle_exception(&self, _request: &Request, error: DispatchError) -> Response {
error!("Handling exception: {}", error);
crate::build_error_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
}
pub fn is_async(&self) -> bool {
self.is_async
}
pub fn set_async(&mut self, is_async: bool) {
self.is_async = is_async;
}
}
impl Default for BaseHandler {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl Handler for BaseHandler {
async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
let has_exception_handler = request
.extensions
.contains::<Arc<dyn reinhardt_http::ExceptionHandler>>();
if has_exception_handler {
return self.handle_request_with_framework_errors(request).await;
}
match self.handle_request_with_errors(request).await {
Ok(response) => Ok(response),
Err(DispatchError::UrlResolution(_)) => Ok(Response::new(StatusCode::NOT_FOUND)),
Err(e) => {
error!("Handler error in BaseHandler::handle: {}", e);
Ok(crate::build_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"Internal Server Error",
))
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use async_trait::async_trait;
use bytes::Bytes;
use hyper::{HeaderMap, Method, Version};
use reinhardt_http::{ExceptionHandler, ExceptionHandlingHandler, Middleware, MiddlewareChain};
use reinhardt_urls::routers::{DefaultRouter, Router, path};
use rstest::rstest;
struct TestHandler {
response_body: String,
}
#[async_trait]
impl Handler for TestHandler {
async fn handle(&self, _req: Request) -> reinhardt_core::exception::Result<Response> {
Ok(Response::ok().with_body(self.response_body.clone()))
}
}
#[tokio::test]
async fn test_base_handler_new() {
let handler = BaseHandler::new();
assert!(handler.is_async());
}
#[tokio::test]
async fn test_base_handler_handle_request() {
let handler = BaseHandler::new();
let request = Request::builder()
.method(Method::GET)
.uri("/")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await;
let resp = response.unwrap();
assert_eq!(resp.status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_base_handler_handle_exception() {
let handler = BaseHandler::new();
let request = Request::builder()
.method(Method::GET)
.uri("/")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let error = DispatchError::View("Test error".to_string());
let response = handler.handle_exception(&request, error).await;
assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn test_handle_exception_does_not_expose_internal_details() {
let handler = BaseHandler::new();
let request = Request::builder()
.method(Method::GET)
.uri("/")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let sensitive_detail = "database connection refused at postgres://admin:secret@db:5432";
let error = DispatchError::Internal(sensitive_detail.to_string());
let response = handler.handle_exception(&request, error).await;
let body = String::from_utf8(response.body.to_vec()).unwrap();
assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
assert!(!body.contains("database"));
assert!(!body.contains("postgres"));
assert!(!body.contains("secret"));
assert_eq!(body, "Internal Server Error");
}
#[tokio::test]
async fn test_handler_impl_does_not_expose_error_in_body() {
struct FailingHandler;
#[async_trait]
impl Handler for FailingHandler {
async fn handle(&self, _req: Request) -> reinhardt_core::exception::Result<Response> {
Err(reinhardt_core::exception::Error::Internal(
"module::secret_handler panicked at /src/app/handlers.rs:42".to_string(),
))
}
}
let mut router = DefaultRouter::new();
let failing = Arc::new(FailingHandler);
let mut route = path("/fail", failing);
route.name = Some("fail".to_string());
router.add_route(route);
let handler = BaseHandler::with_router(Arc::new(router));
let request = Request::builder()
.method(Method::GET)
.uri("/fail")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle(request).await.unwrap();
let body = String::from_utf8(response.body.to_vec()).unwrap();
assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
assert!(!body.contains("panicked"));
assert!(!body.contains("handlers.rs"));
assert!(!body.contains("secret_handler"));
assert_eq!(body, "Internal Server Error");
}
struct TeapotExceptionHandler;
#[async_trait]
impl ExceptionHandler for TeapotExceptionHandler {
async fn handle_exception(
&self,
_request: &Request,
_error: reinhardt_core::exception::Error,
) -> Response {
Response::new(StatusCode::IM_A_TEAPOT).with_body("teapot")
}
}
struct StatusExceptionHandler;
#[async_trait]
impl ExceptionHandler for StatusExceptionHandler {
async fn handle_exception(
&self,
_request: &Request,
error: reinhardt_core::exception::Error,
) -> Response {
let status = StatusCode::from_u16(error.status_code())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
Response::new(status)
}
}
#[rstest]
#[tokio::test]
async fn base_handler_routing_errors_reach_http_exception_handler() {
let base = Arc::new(BaseHandler::with_router(Arc::new(DefaultRouter::new())));
let handler = ExceptionHandlingHandler::new(base, Arc::new(TeapotExceptionHandler));
let request = Request::builder()
.method(Method::GET)
.uri("/missing")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(response.body, Bytes::from_static(b"teapot"));
}
#[rstest]
#[tokio::test]
async fn base_handler_view_errors_reach_http_exception_handler() {
struct FailingHandler;
#[async_trait]
impl Handler for FailingHandler {
async fn handle(
&self,
_request: Request,
) -> reinhardt_core::exception::Result<Response> {
Err(reinhardt_core::exception::Error::Internal(
"view failed".to_owned(),
))
}
}
let mut router = DefaultRouter::new();
router.add_route(path("/fail", Arc::new(FailingHandler)));
let base = Arc::new(BaseHandler::with_router(Arc::new(router)));
let handler = ExceptionHandlingHandler::new(base, Arc::new(TeapotExceptionHandler));
let request = Request::builder()
.method(Method::GET)
.uri("/fail")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(response.body, Bytes::from_static(b"teapot"));
}
#[rstest]
#[tokio::test]
async fn base_handler_preserves_endpoint_error_status_for_http_exception_handler() {
struct AuthenticationHandler;
#[async_trait]
impl Handler for AuthenticationHandler {
async fn handle(
&self,
_request: Request,
) -> reinhardt_core::exception::Result<Response> {
Err(reinhardt_core::exception::Error::Authentication(
"credentials rejected".to_owned(),
))
}
}
let mut router = DefaultRouter::new();
router.add_route(path("/private", Arc::new(AuthenticationHandler)));
let base = Arc::new(BaseHandler::with_router(Arc::new(router)));
let handler = ExceptionHandlingHandler::new(base, Arc::new(StatusExceptionHandler));
let request = Request::builder()
.method(Method::GET)
.uri("/private")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::UNAUTHORIZED);
}
#[rstest]
#[tokio::test]
async fn base_handler_exception_handler_receives_resolved_path_params() {
struct FailingHandler;
#[async_trait]
impl Handler for FailingHandler {
async fn handle(
&self,
_request: Request,
) -> reinhardt_core::exception::Result<Response> {
Err(reinhardt_core::exception::Error::Internal(
"view failed".to_owned(),
))
}
}
struct PassthroughMiddleware;
#[async_trait]
impl Middleware for PassthroughMiddleware {
async fn process(
&self,
request: Request,
next: Arc<dyn Handler>,
) -> reinhardt_core::exception::Result<Response> {
next.handle(request).await
}
}
struct PathParamExceptionHandler {
observed: Arc<Mutex<Option<String>>>,
}
#[async_trait]
impl ExceptionHandler for PathParamExceptionHandler {
async fn handle_exception(
&self,
request: &Request,
_error: reinhardt_core::exception::Error,
) -> Response {
*self.observed.lock().unwrap() = request.path_params.get("id").cloned();
Response::new(StatusCode::IM_A_TEAPOT)
}
}
let observed = Arc::new(Mutex::new(None));
let mut router = DefaultRouter::new();
let mut route = path("/items/{id}", Arc::new(FailingHandler));
route.name = Some("item".to_owned());
router.add_route(route);
let base = Arc::new(BaseHandler::with_router(Arc::new(router)));
let handler = MiddlewareChain::new(base)
.with_middleware(Arc::new(PassthroughMiddleware))
.with_exception_handler(Arc::new(PathParamExceptionHandler {
observed: Arc::clone(&observed),
}));
let request = Request::builder()
.method(Method::GET)
.uri("/items/42")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(*observed.lock().unwrap(), Some("42".to_owned()));
}
#[rstest]
#[tokio::test]
async fn middleware_chain_exception_handler_refreshes_path_params_for_all_error_paths() {
struct Endpoint {
fails: bool,
}
#[async_trait]
impl Handler for Endpoint {
async fn handle(
&self,
_request: Request,
) -> reinhardt_core::exception::Result<Response> {
if self.fails {
Err(reinhardt_core::exception::Error::Internal(
"endpoint failed".to_owned(),
))
} else {
Ok(Response::ok())
}
}
}
struct RecordingExceptionHandler {
observed: Arc<Mutex<Vec<Option<String>>>>,
}
#[async_trait]
impl ExceptionHandler for RecordingExceptionHandler {
async fn handle_exception(
&self,
request: &Request,
_error: reinhardt_core::exception::Error,
) -> Response {
self.observed
.lock()
.unwrap()
.push(request.path_params.get("id").cloned());
Response::new(StatusCode::IM_A_TEAPOT)
}
}
struct PostProcessingMiddleware {
fails: bool,
}
#[async_trait]
impl Middleware for PostProcessingMiddleware {
async fn process(
&self,
request: Request,
next: Arc<dyn Handler>,
) -> reinhardt_core::exception::Result<Response> {
let response = next.handle(request).await?;
if self.fails {
Err(reinhardt_core::exception::Error::Internal(
"middleware failed after next".to_owned(),
))
} else {
Ok(response)
}
}
}
let build_base = |fails| {
let mut router = DefaultRouter::new();
router.add_route(path("/items/{id}", Arc::new(Endpoint { fails })));
Arc::new(BaseHandler::with_router(Arc::new(router))) as Arc<dyn Handler>
};
let request = || {
Request::builder()
.method(Method::GET)
.uri("/items/42")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap()
};
let observed = Arc::new(Mutex::new(Vec::new()));
let exception_handler = || {
Arc::new(RecordingExceptionHandler {
observed: Arc::clone(&observed),
})
};
let bare =
MiddlewareChain::new(build_base(true)).with_exception_handler(exception_handler());
let bare_response = bare.handle(request()).await.unwrap();
let single = MiddlewareChain::new(build_base(false))
.with_middleware(Arc::new(PostProcessingMiddleware { fails: true }))
.with_exception_handler(exception_handler());
let single_response = single.handle(request()).await.unwrap();
let composed = MiddlewareChain::new(build_base(false))
.with_middleware(Arc::new(PostProcessingMiddleware { fails: true }))
.with_middleware(Arc::new(PostProcessingMiddleware { fails: false }))
.with_exception_handler(exception_handler());
let composed_response = composed.handle(request()).await.unwrap();
assert_eq!(bare_response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(single_response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(composed_response.status, StatusCode::IM_A_TEAPOT);
assert_eq!(
*observed.lock().unwrap(),
vec![
Some("42".to_owned()),
Some("42".to_owned()),
Some("42".to_owned())
]
);
}
#[test]
fn test_base_handler_async_mode() {
let mut handler = BaseHandler::new();
assert!(handler.is_async());
handler.set_async(false);
assert!(!handler.is_async());
}
#[tokio::test]
async fn test_base_handler_different_methods() {
let handler = BaseHandler::new();
for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] {
let request = Request::builder()
.method(method)
.uri("/")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await;
assert!(response.is_ok());
}
}
#[tokio::test]
async fn test_base_handler_different_uris() {
let handler = BaseHandler::new();
for path in ["/", "/test", "/api/v1/users", "/admin/login"] {
let request = Request::builder()
.method(Method::GET)
.uri(path)
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await;
assert!(response.is_ok());
}
}
#[tokio::test]
async fn test_handler_with_router() {
let mut router = DefaultRouter::new();
let test_handler = Arc::new(TestHandler {
response_body: "Test response".to_string(),
});
let mut route = path("/test", test_handler);
route.name = Some("test".to_string());
router.add_route(route);
let handler = BaseHandler::with_router(Arc::new(router));
let request = Request::builder()
.method(Method::GET)
.uri("/test")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await;
let resp = response.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let body = String::from_utf8(resp.body.to_vec()).unwrap();
assert_eq!(body, "Test response");
}
#[tokio::test]
async fn test_handler_404_not_found() {
let router = DefaultRouter::new();
let handler = BaseHandler::with_router(Arc::new(router));
let request = Request::builder()
.method(Method::GET)
.uri("/nonexistent")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await;
let resp = response.unwrap();
assert_eq!(resp.status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_handler_multiple_routes() {
let mut router = DefaultRouter::new();
let hello_handler = Arc::new(TestHandler {
response_body: "Hello".to_string(),
});
let mut hello_route = path("/hello", hello_handler);
hello_route.name = Some("hello".to_string());
router.add_route(hello_route);
let world_handler = Arc::new(TestHandler {
response_body: "World".to_string(),
});
let mut world_route = path("/world", world_handler);
world_route.name = Some("world".to_string());
router.add_route(world_route);
let handler = BaseHandler::with_router(Arc::new(router));
let request = Request::builder()
.method(Method::GET)
.uri("/hello")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
assert_eq!(String::from_utf8(response.body.to_vec()).unwrap(), "Hello");
let request = Request::builder()
.method(Method::GET)
.uri("/world")
.version(Version::HTTP_11)
.headers(HeaderMap::new())
.body(Bytes::new())
.build()
.unwrap();
let response = handler.handle_request(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
assert_eq!(String::from_utf8(response.body.to_vec()).unwrap(), "World");
}
}