//! Request handling pipeline.
//!
//! This module orchestrates the full lifecycle of an incoming HTTP request
//! through a chain of handlers that mirror nginx's processing phases:
//!
//! 1. **Rewrite** -- apply URL rewrite / redirect rules.
//! 2. **Static file** -- serve the (possibly rewritten) URI from the document
//! root.
//! 3. **Error page** -- when the upstream handler produces an error status
//! (4xx / 5xx), substitute a custom or built-in error page.
pub mod error_page;
pub mod file_cache;
pub mod index;
pub mod rewrite;
pub mod static_file;
// ---------------------------------------------------------------------------
// Public re-exports
// ---------------------------------------------------------------------------
pub use self::error_page::ErrorPageHandler;
pub use self::index::IndexHandler;
pub use self::rewrite::{RewriteHandler, RewriteResult, RewriteRule};
pub use self::static_file::StaticFileHandler;
use tracing::debug;
use crate::http::request::Request;
use crate::http::response::Response;
use crate::http::status::HttpStatusCode;
// ---------------------------------------------------------------------------
// HandlerChain
// ---------------------------------------------------------------------------
/// Top-level request processor that chains the rewrite, static file, and
/// error page handlers.
///
/// The chain is intentionally stateless -- all handler instances are supplied
/// at call time through a [`HandlerConfig`] reference so that the same chain
/// can be shared across threads without locking.
pub struct HandlerChain;
impl HandlerChain {
/// Process an incoming HTTP request through the full handler pipeline.
///
/// # Processing order
///
/// 1. The rewrite rules in `config` are evaluated against the request
/// URI.
/// - A `Redirect` result produces an HTTP 301/302 response
/// immediately.
/// - A `Return` result forces an arbitrary status code and body.
/// - A `Rewrite` result silently rewrites the URI before continuing.
/// - No match passes the original URI through unchanged.
/// 2. The (possibly rewritten) request is dispatched to the static file
/// handler.
/// 3. If the static file handler returns an error status (>= 400), the
/// error page handler is consulted. It first tries a configured
/// custom page; if none is available it falls back to a built-in
/// default.
pub fn process(request: &Request, config: &HandlerConfig) -> Response {
// ---- Phase 1: rewrite rules ----
let rewrite_result = config.rewrite_handler.apply(&request.uri);
let rewritten_uri: Option<String> = match rewrite_result {
Some(RewriteResult::Redirect(url, code)) => {
debug!(
original = %request.uri,
target = %url,
code = code,
"rewrite: redirect"
);
let status = HttpStatusCode::from(code);
return Response::builder()
.status(status)
.header("Location", &url)
.header("Content-Length", "0")
.build();
}
Some(RewriteResult::Return(code, body)) => {
debug!(
original = %request.uri,
code = code,
"rewrite: forced return"
);
let status = HttpStatusCode::from(code);
return Response::builder()
.status(status)
.header("Content-Type", "text/plain; charset=utf-8")
.header("Content-Length", &body.len().to_string())
.body_str(&body);
}
Some(RewriteResult::Rewrite(new_uri)) => {
debug!(
original = %request.uri,
rewritten = %new_uri,
"rewrite: internal rewrite"
);
Some(new_uri)
}
None => None,
};
// ---- Phase 2: static file serving ----
let effective_request;
let active_request = match &rewritten_uri {
Some(new_uri) => {
effective_request = rewrite_request_uri(request, new_uri);
&effective_request
}
None => request,
};
let response = config.static_file_handler.handle(active_request);
// ---- Phase 3: error page fallback ----
if response.status.is_error() {
let status = response.status;
// Try a custom error page first.
if let Some(custom) = config.error_page_handler.handle(status) {
debug!(status = status.as_u16(), "serving custom error page");
return custom;
}
// Fall back to the built-in default.
debug!(
status = status.as_u16(),
"no custom error page, using built-in default"
);
return ErrorPageHandler::default_error_page(status);
}
response
}
}
// ---------------------------------------------------------------------------
// Handler configuration
// ---------------------------------------------------------------------------
/// Aggregated configuration for the request processing pipeline.
///
/// Each field corresponds to one phase of the [`HandlerChain`] pipeline.
#[derive(Debug, Clone)]
pub struct HandlerConfig {
/// URL rewrite rules evaluated before static file serving.
pub rewrite_handler: RewriteHandler,
/// Serves static files from the document root.
pub static_file_handler: StaticFileHandler,
/// Serves custom error pages for 4xx / 5xx responses.
pub error_page_handler: ErrorPageHandler,
}
impl HandlerConfig {
/// Create a new handler configuration.
pub fn new(
rewrite_handler: RewriteHandler,
static_file_handler: StaticFileHandler,
error_page_handler: ErrorPageHandler,
) -> Self {
Self {
rewrite_handler,
static_file_handler,
error_page_handler,
}
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Produce a clone of `request` with the URI (and derived path/query)
/// replaced by `new_uri`.
///
/// All other fields -- method, version, headers, body, remote address, and
/// connection id -- are preserved from the original request.
fn rewrite_request_uri(request: &Request, new_uri: &str) -> Request {
let mut rewritten = request.clone();
let (path, query) = match new_uri.find('?') {
Some(pos) => (
new_uri[..pos].to_string(),
Some(new_uri[pos + 1..].to_string()),
),
None => (new_uri.to_string(), None),
};
rewritten.uri = new_uri.to_string();
rewritten.path = path;
rewritten.query = query;
rewritten
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::http::request::{Method, Version};
/// Build a minimal GET request for testing.
fn test_request(uri: &str) -> Request {
Request::new(Method::GET, uri.to_string(), Version::Http11)
}
/// Build a `HandlerConfig` with no rewrite rules, a static file handler
/// rooted at `root`, and an empty error page map.
fn test_config(root: std::path::PathBuf) -> HandlerConfig {
HandlerConfig::new(
RewriteHandler::new(vec![]),
StaticFileHandler::with_defaults(root.clone()),
ErrorPageHandler::new(root, std::collections::HashMap::new()),
)
}
// -- rewrite_request_uri ------------------------------------------------
#[test]
fn rewrite_preserves_path_only() {
let req = test_request("/old");
let rewritten = rewrite_request_uri(&req, "/new");
assert_eq!(rewritten.uri, "/new");
assert_eq!(rewritten.path, "/new");
assert!(rewritten.query.is_none());
}
#[test]
fn rewrite_preserves_query_string() {
let req = test_request("/search?q=test&page=1");
let rewritten = rewrite_request_uri(&req, "/results?q=test&page=1");
assert_eq!(rewritten.uri, "/results?q=test&page=1");
assert_eq!(rewritten.path, "/results");
assert_eq!(rewritten.query.as_deref(), Some("q=test&page=1"));
}
#[test]
fn rewrite_preserves_method_and_version() {
let req = test_request("/old");
let rewritten = rewrite_request_uri(&req, "/new");
assert_eq!(rewritten.method, Method::GET);
assert_eq!(rewritten.version, Version::Http11);
}
// -- HandlerChain: rewrite phase ----------------------------------------
#[test]
fn process_redirect_short_circuits() {
let rules = vec![RewriteRule {
pattern: "/old".into(),
replacement: "/new".into(),
redirect: true,
permanent: true,
last: true,
}];
let root = std::env::temp_dir().join("veld_chain_test_redirect");
let _ = std::fs::create_dir_all(&root);
let config = HandlerConfig::new(
RewriteHandler::new(rules),
StaticFileHandler::with_defaults(root.clone()),
ErrorPageHandler::new(root.clone(), std::collections::HashMap::new()),
);
let request = test_request("/old");
let response = HandlerChain::process(&request, &config);
assert!(response.status.is_redirection());
assert_eq!(response.status.as_u16(), 301);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn process_return_forces_response() {
let config = HandlerConfig::new(
RewriteHandler::with_return(503, "Maintenance"),
StaticFileHandler::with_defaults(std::path::PathBuf::from("/tmp")),
ErrorPageHandler::new(
std::path::PathBuf::from("/tmp"),
std::collections::HashMap::new(),
),
);
let request = test_request("/anything");
let response = HandlerChain::process(&request, &config);
assert_eq!(response.status.as_u16(), 503);
}
// -- HandlerChain: error page fallback ----------------------------------
#[test]
fn process_falls_back_to_default_error_page() {
// Request a path that does not exist -> 404 from static file handler
// -> default error page from ErrorPageHandler.
let root = std::env::temp_dir().join("veld_chain_test_fallback");
let _ = std::fs::create_dir_all(&root);
let config = test_config(root.clone());
let request = test_request("/does-not-exist.html");
let response = HandlerChain::process(&request, &config);
// Should be a 404 using the built-in error page.
assert_eq!(response.status.as_u16(), 404);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn process_serves_custom_error_page() {
let root = std::env::temp_dir().join("veld_chain_test_custom_err");
let _ = std::fs::create_dir_all(&root);
// Create a custom 404 page.
std::fs::write(root.join("err_404.html"), "<h1>Custom 404</h1>").unwrap();
let mut error_pages = std::collections::HashMap::new();
error_pages.insert(404u16, "/err_404.html".to_string());
let config = HandlerConfig::new(
RewriteHandler::new(vec![]),
StaticFileHandler::with_defaults(root.clone()),
ErrorPageHandler::new(root.clone(), error_pages),
);
let request = test_request("/does-not-exist.html");
let response = HandlerChain::process(&request, &config);
assert_eq!(response.status.as_u16(), 404);
let _ = std::fs::remove_dir_all(&root);
}
// -- HandlerChain: rewrite + static file integration --------------------
#[test]
fn process_rewrite_then_serve() {
let root = std::env::temp_dir().join("veld_chain_test_rewrite_serve");
let _ = std::fs::create_dir_all(&root);
// Create a file at the rewritten destination.
std::fs::write(root.join("actual.html"), "<h1>Hello</h1>").unwrap();
let rules = vec![RewriteRule {
pattern: "/virtual".into(),
replacement: "/actual.html".into(),
redirect: false,
permanent: false,
last: true,
}];
let config = HandlerConfig::new(
RewriteHandler::new(rules),
StaticFileHandler::with_defaults(root.clone()),
ErrorPageHandler::new(root.clone(), std::collections::HashMap::new()),
);
let request = test_request("/virtual");
let response = HandlerChain::process(&request, &config);
assert_eq!(response.status.as_u16(), 200);
let _ = std::fs::remove_dir_all(&root);
}
// -- HandlerConfig construction -----------------------------------------
#[test]
fn handler_config_new() {
let root = std::path::PathBuf::from("/var/www");
let config = HandlerConfig::new(
RewriteHandler::new(vec![]),
StaticFileHandler::with_defaults(root.clone()),
ErrorPageHandler::new(root, std::collections::HashMap::new()),
);
assert_eq!(config.rewrite_handler.rules().len(), 0);
}
}