use crate::epoll_tuning::EpollConfig;
use crate::exception_filter::{ExceptionFilter, ExceptionFilterChain};
use crate::guard::{Guard, GuardContext};
use crate::http2::{Http2Builder, Http2Config, Http2Stats};
use crate::http3::{Http3Config, Http3Stats};
use crate::logging::{debug, error, info, trace, warn};
use crate::pipeline::{PipelineConfig, PipelineStats};
use crate::route_cache::OptimizedRouter;
use crate::{
Container, Error, HttpRequest, HttpResponse, HttpsConfig, LifecycleManager, Module, Router,
TlsConfig,
};
use http_body_util::{BodyExt, Full, Limited};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, body::Incoming as IncomingBody};
use hyper_util::rt::TokioIo;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
#[cfg(not(feature = "h1-backend"))]
use crate::pipeline::PipelinedHttp1Builder;
#[cfg(not(feature = "h1-backend"))]
use tokio_rustls::TlsAcceptor;
pub struct Application {
pub container: Container,
pub router: Arc<Router>,
pub lifecycle: Arc<LifecycleManager>,
pipeline_config: PipelineConfig,
pipeline_stats: Arc<PipelineStats>,
http2_config: Http2Config,
http2_stats: Arc<Http2Stats>,
http3_config: Http3Config,
http3_stats: Arc<Http3Stats>,
cors_config: Option<Arc<CorsConfig>>,
guards: Vec<ScopedGuard>,
max_body_size: usize,
#[cfg_attr(not(unix), allow(dead_code))]
epoll_config: Option<EpollConfig>,
filter_chain: Option<ExceptionFilterChain>,
}
pub const DEFAULT_MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
#[derive(Clone)]
struct ScopedGuard {
prefix: String,
guard: Arc<dyn Guard>,
}
impl ScopedGuard {
fn matches(&self, path: &str) -> bool {
let prefix = self.prefix.trim_end_matches('/');
if prefix.is_empty() {
return true;
}
path == prefix || path.starts_with(&format!("{}/", prefix))
}
}
#[derive(Clone)]
pub(crate) struct ServeState {
router: Arc<OptimizedRouter>,
cors: Option<Arc<CorsConfig>>,
guards: Arc<[ScopedGuard]>,
max_body_size: usize,
filter_chain: Option<Arc<ExceptionFilterChain>>,
peer: Option<SocketAddr>,
}
impl ServeState {
#[cfg(all(test, feature = "h1-backend"))]
pub(crate) fn for_test(router: Arc<OptimizedRouter>, max_body_size: usize) -> Self {
Self {
router,
cors: None,
guards: Vec::new().into(),
max_body_size,
filter_chain: None,
peer: None,
}
}
#[cfg(all(test, feature = "h1-backend"))]
pub(crate) fn with_cors_for_test(mut self, cors: CorsConfig) -> Self {
self.cors = Some(Arc::new(cors));
self
}
#[cfg(all(test, feature = "h1-backend"))]
pub(crate) fn with_guard_for_test(mut self, guard: Arc<dyn Guard>) -> Self {
self.guards = vec![ScopedGuard {
prefix: String::new(),
guard,
}]
.into();
self
}
#[cfg(all(test, feature = "h1-backend"))]
pub(crate) fn with_filter_chain_for_test(mut self, chain: ExceptionFilterChain) -> Self {
self.filter_chain = Some(Arc::new(chain));
self
}
pub(crate) fn for_peer(&self, peer: SocketAddr) -> Self {
Self {
peer: Some(peer),
..self.clone()
}
}
}
#[derive(Debug, Clone)]
pub struct CorsConfig {
pub allow_origin: String,
pub allow_methods: String,
pub allow_headers: String,
pub allow_credentials: bool,
pub max_age: u32,
}
impl CorsConfig {
pub fn new(origin: impl Into<String>) -> Self {
Self {
allow_origin: origin.into(),
allow_methods: "GET, POST, PUT, DELETE, OPTIONS, PATCH".to_string(),
allow_headers: "Content-Type, Authorization, Accept, X-Requested-With".to_string(),
allow_credentials: false,
max_age: 86400,
}
}
pub fn with_credentials(mut self) -> Self {
self.allow_credentials = true;
self
}
pub fn allow_headers(mut self, headers: impl Into<String>) -> Self {
self.allow_headers = headers.into();
self
}
}
impl Application {
pub fn new(container: Container, router: Router) -> Self {
Self {
container,
router: Arc::new(router),
lifecycle: Arc::new(LifecycleManager::new()),
pipeline_config: PipelineConfig::default(),
pipeline_stats: Arc::new(PipelineStats::new()),
http2_config: Http2Config::default(),
http2_stats: Arc::new(Http2Stats::new()),
http3_config: Http3Config::default(),
http3_stats: Arc::new(Http3Stats::new()),
cors_config: None,
guards: Vec::new(),
max_body_size: DEFAULT_MAX_BODY_SIZE,
epoll_config: None,
filter_chain: None,
}
}
pub fn use_global_filter<F: ExceptionFilter>(mut self, filter: F) -> Self {
let chain = self.filter_chain.take().unwrap_or_default();
self.filter_chain = Some(chain.add_filter(filter));
self
}
pub fn with_cors(mut self, config: CorsConfig) -> Self {
self.cors_config = Some(Arc::new(config));
self
}
pub fn with_guard(mut self, guard: Arc<dyn Guard>) -> Self {
self.guards.push(ScopedGuard {
prefix: String::new(),
guard,
});
self
}
pub fn with_max_body_size(mut self, bytes: usize) -> Self {
self.max_body_size = bytes;
self
}
pub fn with_socket_tuning(mut self, config: EpollConfig) -> Self {
self.epoll_config = Some(config);
self
}
fn serve_state(&self) -> ServeState {
ServeState {
router: Arc::new(OptimizedRouter::from_router(&self.router)),
cors: self.cors_config.clone(),
guards: self.guards.clone().into(),
max_body_size: self.max_body_size,
filter_chain: self.filter_chain.clone().map(Arc::new),
peer: None,
}
}
pub fn with_pipeline_config(mut self, config: PipelineConfig) -> Self {
self.pipeline_config = config;
self
}
pub fn pipeline_stats(&self) -> Arc<PipelineStats> {
Arc::clone(&self.pipeline_stats)
}
pub fn pipeline_config(&self) -> &PipelineConfig {
&self.pipeline_config
}
pub fn with_http2_config(mut self, config: Http2Config) -> Self {
self.http2_config = config;
self
}
pub fn http2_stats(&self) -> Arc<Http2Stats> {
Arc::clone(&self.http2_stats)
}
pub fn http2_config(&self) -> &Http2Config {
&self.http2_config
}
pub fn with_http3_config(mut self, config: Http3Config) -> Self {
self.http3_config = config;
self
}
pub fn http3_stats(&self) -> Arc<Http3Stats> {
Arc::clone(&self.http3_stats)
}
pub fn http3_config(&self) -> &Http3Config {
&self.http3_config
}
pub async fn create<M: Module + Default>() -> Self {
info!("Bootstrapping Armature application");
debug!(
module_type = std::any::type_name::<M>(),
"Creating application from root module"
);
let container = Container::new();
debug!("DI container initialized");
let mut router = Router::new();
debug!("Router initialized");
let lifecycle = Arc::new(LifecycleManager::new());
debug!("Lifecycle manager initialized");
container.attach_lifecycle(&lifecycle);
let root_module = M::default();
debug!("Root module instantiated");
info!("Registering modules and dependencies");
let mut guards: Vec<ScopedGuard> = Vec::new();
let mut visited = std::collections::HashSet::new();
Self::register_module(
&container,
&mut router,
&mut guards,
&mut visited,
&root_module,
);
info!("Executing lifecycle hooks");
debug!("Calling OnModuleInit hooks");
if let Err(errors) = lifecycle.call_module_init_hooks().await {
warn!(error_count = errors.len(), "Some module init hooks failed");
for (name, error) in errors {
error!(hook_name = %name, error = %error, "Module init hook failed");
}
} else {
debug!("All OnModuleInit hooks completed successfully");
}
debug!("Calling OnApplicationBootstrap hooks");
if let Err(errors) = lifecycle.call_bootstrap_hooks().await {
warn!(error_count = errors.len(), "Some bootstrap hooks failed");
for (name, error) in errors {
error!(hook_name = %name, error = %error, "Bootstrap hook failed");
}
} else {
debug!("All OnApplicationBootstrap hooks completed successfully");
}
info!("Application bootstrap complete");
Self {
container,
router: Arc::new(router),
lifecycle,
pipeline_config: PipelineConfig::default(),
pipeline_stats: Arc::new(PipelineStats::new()),
http2_config: Http2Config::default(),
http2_stats: Arc::new(Http2Stats::new()),
http3_config: Http3Config::default(),
http3_stats: Arc::new(Http3Stats::new()),
cors_config: None,
guards,
max_body_size: DEFAULT_MAX_BODY_SIZE,
epoll_config: None,
filter_chain: None,
}
}
pub fn lifecycle(&self) -> &Arc<LifecycleManager> {
&self.lifecycle
}
pub async fn shutdown(
&self,
signal: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(signal = ?signal, "Gracefully shutting down application");
debug!("Calling BeforeApplicationShutdown hooks");
if let Err(errors) = self
.lifecycle
.call_before_shutdown_hooks(signal.clone())
.await
{
warn!(
error_count = errors.len(),
"Some before shutdown hooks failed"
);
for (name, error) in errors {
error!(hook_name = %name, error = %error, "Before shutdown hook failed");
}
} else {
debug!("All BeforeApplicationShutdown hooks completed successfully");
}
debug!("Calling OnApplicationShutdown hooks");
if let Err(errors) = self.lifecycle.call_shutdown_hooks(signal.clone()).await {
warn!(error_count = errors.len(), "Some shutdown hooks failed");
for (name, error) in errors {
error!(hook_name = %name, error = %error, "Shutdown hook failed");
}
} else {
debug!("All OnApplicationShutdown hooks completed successfully");
}
debug!("Calling OnModuleDestroy hooks");
if let Err(errors) = self.lifecycle.call_module_destroy_hooks().await {
warn!(
error_count = errors.len(),
"Some module destroy hooks failed"
);
for (name, error) in errors {
error!(hook_name = %name, error = %error, "Module destroy hook failed");
}
} else {
debug!("All OnModuleDestroy hooks completed successfully");
}
info!("Application shutdown complete");
Ok(())
}
pub fn init_logging() -> Option<crate::logging::tracing_appender::non_blocking::WorkerGuard> {
crate::logging::LogConfig::default().init()
}
pub fn init_logging_with_config(
config: crate::logging::LogConfig,
) -> Option<crate::logging::tracing_appender::non_blocking::WorkerGuard> {
config.init()
}
fn register_module(
container: &Container,
router: &mut Router,
guards: &mut Vec<ScopedGuard>,
visited: &mut std::collections::HashSet<std::any::TypeId>,
module: &dyn Module,
) {
let module_id = module.module_type_id();
let module_type = module.module_type_name();
if !visited.insert(module_id) {
debug!(
module_type = module_type,
"Module already registered, skipping"
);
return;
}
debug!(module_type = module_type, "Registering module");
let imports = module.imports();
if !imports.is_empty() {
debug!(
module_type = module_type,
import_count = imports.len(),
"Registering imported modules"
);
for imported_module in imports {
Self::register_module(container, router, guards, visited, imported_module.as_ref());
}
}
let re_exports = module.re_exports();
if !re_exports.is_empty() {
debug!(
module_type = module_type,
re_export_count = re_exports.len(),
"Registering re-exported modules"
);
for re_exported_module in re_exports {
Self::register_module(
container,
router,
guards,
visited,
re_exported_module.as_ref(),
);
}
}
let providers = module.providers();
debug!(
module_type = module_type,
provider_count = providers.len(),
"Registering providers"
);
for provider_reg in providers {
(provider_reg.register_fn)(container);
debug!(
module_type = module_type,
provider = provider_reg.type_name,
"Provider registered"
);
}
let guard_regs = module.guards();
if !guard_regs.is_empty() {
let controller_paths: Vec<&'static str> =
module.controllers().iter().map(|c| c.base_path).collect();
debug!(
module_type = module_type,
guard_count = guard_regs.len(),
controller_count = controller_paths.len(),
"Registering guards"
);
for guard_reg in guard_regs {
match (guard_reg.factory)(container) {
Ok(guard) => {
if controller_paths.is_empty() {
warn!(
module_type = module_type,
guard = guard_reg.type_name,
"Module declares a guard but registers no controllers; \
the guard is inert and will not run for any request"
);
} else {
for base_path in &controller_paths {
guards.push(ScopedGuard {
prefix: base_path.to_string(),
guard: guard.clone(),
});
}
debug!(
module_type = module_type,
guard = guard_reg.type_name,
scoped_to = ?controller_paths,
"Guard registered (scoped to module's controller base paths)"
);
}
}
Err(e) => {
error!(
module_type = module_type,
guard = guard_reg.type_name,
error = %e,
"Failed to instantiate guard"
);
}
}
}
}
let controllers = module.controllers();
debug!(
module_type = module_type,
controller_count = controllers.len(),
"Registering controllers"
);
for controller_reg in controllers {
match (controller_reg.factory)(container) {
Ok(controller_instance) => {
if let Err(e) =
(controller_reg.route_registrar)(container, router, controller_instance)
{
error!(
module_type = module_type,
controller = controller_reg.type_name,
error = %e,
"Failed to register routes for controller"
);
} else {
debug!(
module_type = module_type,
controller = controller_reg.type_name,
base_path = controller_reg.base_path,
"Controller registered"
);
}
}
Err(e) => {
error!(
module_type = module_type,
controller = controller_reg.type_name,
error = %e,
"Failed to instantiate controller"
);
}
}
}
debug!(module_type = module_type, "Module registration complete");
}
pub async fn listen(self, port: u16) -> Result<(), Error> {
self.listen_on((std::net::Ipv4Addr::UNSPECIFIED, port))
.await
}
pub async fn listen_on(self, addr: impl Into<SocketAddr>) -> Result<(), Error> {
let addr = addr.into();
#[cfg(feature = "h1-backend")]
let served = self.listen_on_h1(addr).await;
#[cfg(not(feature = "h1-backend"))]
let served = self.listen_on_hyper(addr).await;
served
}
#[cfg(feature = "h1-backend")]
async fn listen_on_h1(self, addr: SocketAddr) -> Result<(), Error> {
let state = self.serve_state();
let cfg = crate::h1_backend::h1_config(addr, &self.pipeline_config, None);
crate::h1_backend::serve(cfg, state, None).await
}
#[cfg(not(feature = "h1-backend"))]
async fn listen_on_hyper(self, addr: SocketAddr) -> Result<(), Error> {
debug!(address = %addr, "Binding to address");
let listener = TcpListener::bind(addr).await?;
#[cfg(unix)]
let socket_tuning = self.epoll_config.clone();
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(listener.as_raw_fd(), tuning, "listener");
}
info!(
address = %addr,
pipeline_mode = ?self.pipeline_config.mode,
pipeline_flush = self.pipeline_config.pipeline_flush,
max_concurrent = self.pipeline_config.max_concurrent,
"HTTP server listening with pipelining enabled"
);
let state = self.serve_state();
let pipeline_builder = PipelinedHttp1Builder::with_stats(
self.pipeline_config.clone(),
Arc::clone(&self.pipeline_stats),
);
let pipeline_stats = Arc::clone(&self.pipeline_stats);
loop {
let (stream, client_addr) = listener.accept().await?;
trace!(client_address = %client_addr, "Connection accepted");
if pipeline_builder.config().tcp_nodelay
&& let Err(e) = stream.set_nodelay(true)
{
trace!(error = %e, "Failed to set TCP_NODELAY");
}
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(stream.as_raw_fd(), tuning, "accepted connection");
}
let io = TokioIo::new(stream);
let state = state.for_peer(client_addr);
let http_builder = pipeline_builder.configure_hyper_builder();
let stats = Arc::clone(&pipeline_stats);
stats.connection_opened();
tokio::spawn(async move {
let stats_for_close = Arc::clone(&stats);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
let stats = Arc::clone(&stats);
async move {
stats.request_processed();
handle_request(req, state).await
}
});
if let Err(err) = http_builder.serve_connection(io, service).await {
error!(error = %err, client = %client_addr, "Error serving connection");
}
stats_for_close.connection_closed();
});
}
}
pub async fn listen_https(self, port: u16, tls_config: TlsConfig) -> Result<(), Error> {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
#[cfg(feature = "h1-backend")]
let served = self.listen_https_h1(addr, tls_config, false).await;
#[cfg(not(feature = "h1-backend"))]
let served = self.listen_https_hyper(addr, tls_config).await;
served
}
#[cfg(feature = "h1-backend")]
async fn listen_https_h1(
self,
addr: SocketAddr,
tls_config: TlsConfig,
with_h2: bool,
) -> Result<(), Error> {
let state = self.serve_state();
let tls = if with_h2 {
tls_config.server_config
} else {
without_h2_alpn(tls_config.server_config)
};
let cfg = crate::h1_backend::h1_config(addr, &self.pipeline_config, None).with_tls(tls);
let h2 = with_h2.then(|| {
Http2Builder::with_stats(self.http2_config.clone(), Arc::clone(&self.http2_stats))
.configure_hyper_builder()
});
crate::h1_backend::serve(cfg, state, h2).await
}
#[cfg(not(feature = "h1-backend"))]
async fn listen_https_hyper(
self,
addr: SocketAddr,
tls_config: TlsConfig,
) -> Result<(), Error> {
debug!(address = %addr, "Binding to address (HTTPS)");
let listener = TcpListener::bind(addr).await?;
#[cfg(unix)]
let socket_tuning = self.epoll_config.clone();
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(listener.as_raw_fd(), tuning, "listener");
}
info!(
address = %addr,
pipeline_mode = ?self.pipeline_config.mode,
pipeline_flush = self.pipeline_config.pipeline_flush,
"HTTPS server listening with pipelining enabled"
);
let acceptor = TlsAcceptor::from(without_h2_alpn(tls_config.server_config));
let state = self.serve_state();
let pipeline_builder = PipelinedHttp1Builder::with_stats(
self.pipeline_config.clone(),
Arc::clone(&self.pipeline_stats),
);
let pipeline_stats = Arc::clone(&self.pipeline_stats);
loop {
let (stream, client_addr) = listener.accept().await?;
trace!(client_address = %client_addr, "HTTPS connection accepted");
if pipeline_builder.config().tcp_nodelay
&& let Err(e) = stream.set_nodelay(true)
{
trace!(error = %e, "Failed to set TCP_NODELAY");
}
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(stream.as_raw_fd(), tuning, "accepted connection");
}
let acceptor = acceptor.clone();
let state = state.for_peer(client_addr);
let http_builder = pipeline_builder.configure_hyper_builder();
let stats = Arc::clone(&pipeline_stats);
stats.connection_opened();
tokio::spawn(async move {
let stats_for_close = Arc::clone(&stats);
match acceptor.accept(stream).await {
Ok(tls_stream) => {
debug!(client = %client_addr, "TLS handshake successful");
let io = TokioIo::new(tls_stream);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
let stats = Arc::clone(&stats);
async move {
stats.request_processed();
handle_request(req, state).await
}
});
if let Err(err) = http_builder.serve_connection(io, service).await {
error!(error = %err, client = %client_addr, "Error serving HTTPS connection");
}
}
Err(err) => {
error!(error = %err, client = %client_addr, "TLS handshake failed");
}
}
stats_for_close.connection_closed();
});
}
}
pub async fn listen_with_config(self, config: HttpsConfig) -> Result<(), Error> {
let state = self.serve_state();
if let Some(ref http_addr) = config.http_redirect_addr {
let https_port = config
.https_addr
.split(':')
.next_back()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(443);
let http_addr = http_addr.clone();
tokio::spawn(async move {
if let Err(e) = start_http_redirect_server(&http_addr, https_port).await {
eprintln!("HTTP redirect server failed: {}", e);
}
});
}
let https_addr: SocketAddr = config
.https_addr
.parse()
.map_err(|e| Error::Internal(format!("Invalid HTTPS address: {}", e)))?;
#[cfg(feature = "h1-backend")]
let served = self.listen_with_config_h1(https_addr, config, state).await;
#[cfg(not(feature = "h1-backend"))]
let served = self
.listen_with_config_hyper(https_addr, config, state)
.await;
served
}
#[cfg(feature = "h1-backend")]
async fn listen_with_config_h1(
self,
https_addr: SocketAddr,
config: HttpsConfig,
state: ServeState,
) -> Result<(), Error> {
let redirecting = config.http_redirect_addr.is_some();
let cfg = crate::h1_backend::h1_config(https_addr, &self.pipeline_config, None)
.with_tls(without_h2_alpn(config.tls.server_config));
crate::h1_backend::serve::serve_bound(cfg, state, None, move |addr, _| {
println!("🔒 HTTPS Server listening on https://{}", addr);
if redirecting {
println!("↪️ HTTP redirect server enabled");
}
})
.await
}
#[cfg(not(feature = "h1-backend"))]
async fn listen_with_config_hyper(
self,
https_addr: SocketAddr,
config: HttpsConfig,
state: ServeState,
) -> Result<(), Error> {
let listener = TcpListener::bind(https_addr).await?;
#[cfg(unix)]
let socket_tuning = self.epoll_config.clone();
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(listener.as_raw_fd(), tuning, "listener");
}
println!("🔒 HTTPS Server listening on https://{}", https_addr);
if config.http_redirect_addr.is_some() {
println!("↪️ HTTP redirect server enabled");
}
let acceptor = TlsAcceptor::from(without_h2_alpn(config.tls.server_config));
loop {
let (stream, client_addr) = listener.accept().await?;
trace!(client_address = %client_addr, "TLS connection accepted");
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(stream.as_raw_fd(), tuning, "accepted connection");
}
let acceptor = acceptor.clone();
let state = state.for_peer(client_addr);
tokio::spawn(async move {
match acceptor.accept(stream).await {
Ok(tls_stream) => {
let io = TokioIo::new(tls_stream);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
async move { handle_request(req, state).await }
});
if let Err(err) = http1::Builder::new().serve_connection(io, service).await
{
eprintln!("Error serving HTTPS connection: {:?}", err);
}
}
Err(err) => {
eprintln!("TLS handshake failed: {:?}", err);
}
}
});
}
}
pub async fn listen_h2c(self, port: u16) -> Result<(), Error> {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
debug!(address = %addr, "Binding to address (HTTP/2 cleartext)");
let listener = TcpListener::bind(addr).await?;
#[cfg(unix)]
let socket_tuning = self.epoll_config.clone();
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(listener.as_raw_fd(), tuning, "listener");
}
info!(
address = %addr,
max_concurrent_streams = self.http2_config.max_concurrent_streams,
"HTTP/2 cleartext server listening (h2c)"
);
warn!("HTTP/2 cleartext (h2c) is not recommended for production. Use HTTPS.");
let state = self.serve_state();
let h2_builder =
Http2Builder::with_stats(self.http2_config.clone(), Arc::clone(&self.http2_stats));
let h2_stats = Arc::clone(&self.http2_stats);
loop {
let (stream, client_addr) = listener.accept().await?;
trace!(client_address = %client_addr, "HTTP/2 connection accepted");
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(stream.as_raw_fd(), tuning, "accepted connection");
}
let io = TokioIo::new(stream);
let state = state.for_peer(client_addr);
let http_builder = h2_builder.configure_hyper_builder();
let stats = Arc::clone(&h2_stats);
stats.connection_opened();
tokio::spawn(async move {
let stats_for_close = Arc::clone(&stats);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
let stats = Arc::clone(&stats);
async move {
stats.request_processed();
handle_request(req, state).await
}
});
if let Err(err) = http_builder.serve_connection(io, service).await {
error!(error = %err, client = %client_addr, "Error serving HTTP/2 connection");
}
stats_for_close.connection_closed();
});
}
}
pub async fn listen_https_h2(self, port: u16, tls_config: TlsConfig) -> Result<(), Error> {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
#[cfg(feature = "h1-backend")]
let served = self.listen_https_h1(addr, tls_config, true).await;
#[cfg(not(feature = "h1-backend"))]
let served = self.listen_https_h2_hyper(addr, tls_config).await;
served
}
#[cfg(not(feature = "h1-backend"))]
async fn listen_https_h2_hyper(
self,
addr: SocketAddr,
tls_config: TlsConfig,
) -> Result<(), Error> {
debug!(address = %addr, "Binding to address (HTTPS with HTTP/2)");
let listener = TcpListener::bind(addr).await?;
#[cfg(unix)]
let socket_tuning = self.epoll_config.clone();
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(listener.as_raw_fd(), tuning, "listener");
}
info!(
address = %addr,
max_concurrent_streams = self.http2_config.max_concurrent_streams,
pipeline_mode = ?self.pipeline_config.mode,
"HTTPS server listening with HTTP/2 and HTTP/1.1 (ALPN)"
);
let acceptor = TlsAcceptor::from(tls_config.server_config);
let state = self.serve_state();
let h1_builder = PipelinedHttp1Builder::with_stats(
self.pipeline_config.clone(),
Arc::clone(&self.pipeline_stats),
);
let h2_builder =
Http2Builder::with_stats(self.http2_config.clone(), Arc::clone(&self.http2_stats));
let h1_stats = Arc::clone(&self.pipeline_stats);
let h2_stats = Arc::clone(&self.http2_stats);
loop {
let (stream, client_addr) = listener.accept().await?;
trace!(client_address = %client_addr, "Connection accepted, starting TLS handshake");
#[cfg(unix)]
if let Some(ref tuning) = socket_tuning {
use std::os::unix::io::AsRawFd;
apply_socket_tuning(stream.as_raw_fd(), tuning, "accepted connection");
}
let acceptor = acceptor.clone();
let state = state.for_peer(client_addr);
let h1_builder_ref = h1_builder.configure_hyper_builder();
let h2_builder_ref = h2_builder.configure_hyper_builder();
let h1_stats = Arc::clone(&h1_stats);
let h2_stats = Arc::clone(&h2_stats);
tokio::spawn(async move {
match acceptor.accept(stream).await {
Ok(tls_stream) => {
let (_, session) = tls_stream.get_ref();
let protocol = session.alpn_protocol();
let is_h2 = protocol.map(|p| p == b"h2").unwrap_or(false);
if is_h2 {
debug!(client = %client_addr, "Using HTTP/2 (ALPN negotiated h2)");
h2_stats.connection_opened();
let io = TokioIo::new(tls_stream);
let stats = Arc::clone(&h2_stats);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
let stats = Arc::clone(&stats);
async move {
stats.request_processed();
handle_request(req, state).await
}
});
if let Err(err) = h2_builder_ref.serve_connection(io, service).await {
error!(error = %err, client = %client_addr, "Error serving HTTP/2 connection");
}
h2_stats.connection_closed();
} else {
debug!(client = %client_addr, "Using HTTP/1.1 (ALPN fallback)");
h1_stats.connection_opened();
let io = TokioIo::new(tls_stream);
let stats = Arc::clone(&h1_stats);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
let stats = Arc::clone(&stats);
async move {
stats.request_processed();
handle_request(req, state).await
}
});
if let Err(err) = h1_builder_ref.serve_connection(io, service).await {
error!(error = %err, client = %client_addr, "Error serving HTTP/1.1 connection");
}
h1_stats.connection_closed();
}
}
Err(err) => {
error!(error = %err, client = %client_addr, "TLS handshake failed");
}
}
});
}
}
#[cfg(feature = "http3")]
pub async fn listen_h3(self, port: u16, tls_config: TlsConfig) -> Result<(), Error> {
use crate::http3::Http3Server;
let addr = SocketAddr::from(([0, 0, 0, 0], port));
info!(
address = %addr,
max_concurrent_streams = self.http3_config.max_concurrent_bidi_streams,
enable_0rtt = self.http3_config.enable_0rtt,
"Starting HTTP/3 (QUIC) server"
);
let optimized = Arc::new(crate::route_cache::OptimizedRouter::from_router(
&self.router,
));
let server = Http3Server::new(self.http3_config.clone(), optimized);
server.listen(addr, tls_config.server_config).await
}
#[cfg(feature = "http3")]
pub async fn listen_dual_stack(self, port: u16, tls_config: TlsConfig) -> Result<(), Error> {
use crate::http3::Http3Server;
let addr = SocketAddr::from(([0, 0, 0, 0], port));
info!(
address = %addr,
"Starting dual-stack server (HTTP/3 + HTTPS)"
);
let tls_config_h3 = tls_config.clone();
let router_h3 = Arc::new(crate::route_cache::OptimizedRouter::from_router(
&self.router,
));
let http3_config = self.http3_config.clone();
let h3_handle = tokio::spawn(async move {
let server = Http3Server::new(http3_config, router_h3);
if let Err(e) = server.listen(addr, tls_config_h3.server_config).await {
error!(error = %e, "HTTP/3 server error");
}
});
let https_handle = tokio::spawn(async move {
if let Err(e) = self.listen_https_h2(port, tls_config).await {
error!(error = %e, "HTTPS server error");
}
});
tokio::select! {
_ = h3_handle => {
warn!("HTTP/3 server stopped");
}
_ = https_handle => {
warn!("HTTPS server stopped");
}
}
Ok(())
}
pub fn container(&self) -> &Container {
&self.container
}
}
fn without_h2_alpn(tls: Arc<rustls::ServerConfig>) -> Arc<rustls::ServerConfig> {
if !tls.alpn_protocols.iter().any(|p| p.as_slice() == b"h2") {
return tls;
}
let mut stripped = (*tls).clone();
stripped.alpn_protocols.retain(|p| p.as_slice() != b"h2");
Arc::new(stripped)
}
#[cfg(unix)]
fn apply_socket_tuning(fd: std::os::unix::io::RawFd, config: &EpollConfig, socket: &'static str) {
if let Err(e) = crate::epoll_tuning::configure_socket(fd, config) {
warn!(error = %e, socket, "Failed to apply socket tuning");
}
}
async fn start_http_redirect_server(addr: &str, https_port: u16) -> Result<(), Error> {
let addr: SocketAddr = addr
.parse()
.map_err(|e| Error::Internal(format!("Invalid HTTP redirect address: {}", e)))?;
let listener = TcpListener::bind(addr).await?;
println!("↪️ HTTP redirect server listening on http://{}", addr);
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let service = service_fn(move |req: Request<IncomingBody>| async move {
let host = req
.headers()
.get("host")
.and_then(|h| h.to_str().ok())
.unwrap_or("localhost");
let host_without_port = host.split(':').next().unwrap_or(host);
let location = if https_port == 443 {
format!("https://{}{}", host_without_port, req.uri().path())
} else {
format!(
"https://{}:{}{}",
host_without_port,
https_port,
req.uri().path()
)
};
let response = Response::builder()
.status(301)
.header("Location", location)
.body(Full::new(bytes::Bytes::from("Redirecting to HTTPS...")))
.unwrap();
Ok::<_, hyper::Error>(response)
});
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
eprintln!("Error serving HTTP redirect: {:?}", err);
}
});
}
}
pub(crate) async fn handle_request(
req: Request<IncomingBody>,
state: ServeState,
) -> Result<Response<Full<bytes::Bytes>>, hyper::Error> {
use std::time::Instant;
let start = Instant::now();
let method = crate::Method::from(req.method().as_str());
let target = req
.uri()
.path_and_query()
.map_or_else(|| req.uri().path().to_owned(), |pq| pq.as_str().to_owned());
let mut armature_req = HttpRequest::new(method.clone(), target).with_peer(state.peer);
let target_handle = armature_req.path.clone();
let path = target_handle
.split_once('?')
.map_or(target_handle.as_str(), |(p, _)| p);
if let Some(preflight) = cors_preflight(
&method,
|| req.headers().contains_key("access-control-request-method"),
&state,
) {
return Ok(to_hyper_response_raw(preflight, &method, path));
}
for (name, value) in req.headers() {
armature_req.headers.append(
name.as_str(),
bytes::Bytes::copy_from_slice(value.as_bytes()),
);
}
let declared_len = declared_content_length(armature_req.headers.get("content-length"));
if let Some(rejection) = declared_length_rejection(declared_len, &method, path, &state) {
return Ok(to_hyper_response(
rejection,
state.cors.as_deref(),
&method,
path,
));
}
let limited = Limited::new(req.into_body(), state.max_body_size);
let body_bytes = match limited.collect().await {
Ok(collected) => collected.to_bytes(),
Err(err) if err.is::<http_body_util::LengthLimitError>() => {
warn!(
method = %method,
path = %path,
limit = state.max_body_size,
"Request body exceeds configured limit"
);
return Ok(to_hyper_response(
payload_too_large_response(),
state.cors.as_deref(),
&method,
path,
));
}
Err(err) => match err.downcast::<hyper::Error>() {
Ok(hyper_err) => return Err(*hyper_err),
Err(other) => {
warn!(method = %method, path = %path, error = %other, "Failed to read request body");
return Ok(to_hyper_response(
HttpResponse::new(400),
state.cors.as_deref(),
&method,
path,
));
}
},
};
let body_size = body_bytes.len();
if body_size > 0 {
armature_req.set_body_bytes(body_bytes);
trace!(body_size = body_size, "Request body received (zero-copy)");
}
Ok(to_hyper_response(
dispatch_request(armature_req, &state, start).await,
state.cors.as_deref(),
&method,
path,
))
}
#[cfg(feature = "h1-backend")]
pub(crate) async fn dispatch_via_h1(
req: armature_h1::Request,
state: ServeState,
) -> armature_h1::Response {
use crate::h1_backend::bridge::{request_from_head, to_h1_response};
use std::time::Instant;
let start = Instant::now();
let armature_h1::Request {
head,
mut body,
peer,
} = req;
let method = head.method.clone();
if let Some(preflight) = cors_preflight(
&method,
|| {
head.headers
.iter()
.any(|(id, _)| id.as_str() == "access-control-request-method")
},
&state,
) {
return to_h1_response(preflight, None, &method, head.path());
}
let declared_len = declared_content_length(head.get_str(&armature_h1::HeaderId::ContentLength));
if let Some(rejection) = declared_length_rejection(declared_len, &method, head.path(), &state) {
return to_h1_response(rejection, state.cors.as_deref(), &method, head.path());
}
let body_bytes = match body.collect(state.max_body_size as u64).await {
Ok(b) => b,
Err(err) => {
let status = err.status();
warn!(
method = %method,
path = head.path(),
error = %err,
status,
"Failed to read request body"
);
let response = if status == 413 {
payload_too_large_response()
} else {
HttpResponse::new(status)
};
return to_h1_response(response, state.cors.as_deref(), &method, head.path());
}
};
let mut armature_req = request_from_head(head, peer);
if !body_bytes.is_empty() {
let body_size = body_bytes.len();
armature_req.set_body_bytes(body_bytes);
trace!(body_size = body_size, "Request body received (zero-copy)");
}
let target_handle = armature_req.path.clone();
let path = target_handle
.split_once('?')
.map_or(target_handle.as_str(), |(p, _)| p);
to_h1_response(
dispatch_request(armature_req, &state, start).await,
state.cors.as_deref(),
&method,
path,
)
}
fn cors_preflight(
method: &crate::Method,
is_preflight: impl FnOnce() -> bool,
state: &ServeState,
) -> Option<HttpResponse> {
let cors = state.cors.as_deref()?;
if method != "OPTIONS" {
return None;
}
if !is_preflight() {
return None;
}
let mut response = HttpResponse::new(204);
response.headers.insert(
"Access-Control-Allow-Origin".into(),
cors.allow_origin.clone(),
);
response.headers.insert(
"Access-Control-Allow-Methods".into(),
cors.allow_methods.clone(),
);
response.headers.insert(
"Access-Control-Allow-Headers".into(),
cors.allow_headers.clone(),
);
response
.headers
.insert("Access-Control-Max-Age".into(), cors.max_age.to_string());
if response_wire::cors_additions(None, cors).credentials {
response.headers.insert(
"Access-Control-Allow-Credentials".into(),
"true".to_string(),
);
}
Some(response)
}
fn declared_content_length(raw: Option<&str>) -> Option<usize> {
raw?.split(',').next()?.trim().parse::<usize>().ok()
}
fn declared_length_rejection(
declared_len: Option<usize>,
method: &crate::Method,
path: &str,
state: &ServeState,
) -> Option<HttpResponse> {
let declared_len = declared_len?;
if body_within_limit(declared_len, state.max_body_size) {
return None;
}
warn!(
method = %method,
path = %path,
limit = state.max_body_size,
declared_len,
"Request Content-Length exceeds configured limit"
);
Some(payload_too_large_response())
}
async fn dispatch_request(
mut armature_req: HttpRequest,
state: &ServeState,
start: std::time::Instant,
) -> HttpResponse {
let method = armature_req.method.clone();
let target_handle = armature_req.path.clone();
let path = target_handle
.split_once('?')
.map_or(target_handle.as_str(), |(p, _)| p);
trace!(
method = %method,
path = %path,
header_count = armature_req.headers.len(),
"Incoming request"
);
let filter_ctx_request = state.filter_chain.as_ref().map(|_| armature_req.clone());
if !state.guards.is_empty() {
match evaluate_scoped_guards(&state.guards, path, armature_req).await {
Ok(req) => armature_req = req,
Err(GuardRejection::Reject) => {
warn!(method = %method, path = %path, "Request rejected by guard");
let body = serde_json::json!({
"error": "Forbidden",
"status": 403,
});
return HttpResponse::new(403)
.with_json(&body)
.unwrap_or_else(|_| HttpResponse::new(403));
}
Err(GuardRejection::Error(err)) => {
warn!(method = %method, path = %path, error = %err, "Guard returned an error");
return respond_to_error(err, filter_ctx_request, state.filter_chain.clone()).await;
}
}
}
debug!(method = %method, path = %path, "Routing request");
let response = match state.router.route(armature_req).await {
Ok(resp) => {
debug!(method = %method, path = %path, status = resp.status, "Request handled successfully");
resp
}
Err(err) => {
warn!(method = %method, path = %path, error = %err, "Request handling failed");
respond_to_error(err, filter_ctx_request, state.filter_chain.clone()).await
}
};
let duration = start.elapsed();
debug!(
method = %method,
path = %path,
status = response.status,
duration_ms = duration.as_millis(),
"Request completed"
);
response
}
fn error_response(err: &Error) -> HttpResponse {
err.to_client_response()
}
const DEFAULT_EXCEPTION_FILTER_TIMEOUT: Duration = Duration::from_secs(5);
async fn respond_to_error(
err: Error,
ctx_request: Option<HttpRequest>,
filter_chain: Option<Arc<ExceptionFilterChain>>,
) -> HttpResponse {
respond_to_error_with_timeout(
err,
ctx_request,
filter_chain,
DEFAULT_EXCEPTION_FILTER_TIMEOUT,
)
.await
}
async fn respond_to_error_with_timeout(
err: Error,
ctx_request: Option<HttpRequest>,
filter_chain: Option<Arc<ExceptionFilterChain>>,
filter_timeout: Duration,
) -> HttpResponse {
match (filter_chain, ctx_request) {
(Some(chain), Some(request)) => {
let fallback = error_response(&err);
let task = tokio::spawn(async move { chain.handle(&err, &request).await });
let abort_handle = task.abort_handle();
match tokio::time::timeout(filter_timeout, task).await {
Ok(Ok(response)) => response,
Ok(Err(join_err)) => {
error!(
error = %join_err,
"Exception filter task panicked; falling back to the default error response"
);
fallback
}
Err(_elapsed) => {
abort_handle.abort();
warn!(
timeout_secs = filter_timeout.as_secs_f64(),
"Exception filter chain timed out; falling back to the default error response"
);
fallback
}
}
}
_ => {
error_response(&err)
}
}
}
fn body_within_limit(len: usize, max: usize) -> bool {
len <= max
}
fn payload_too_large_response() -> HttpResponse {
let body = serde_json::json!({
"error": "Payload Too Large",
"status": 413,
});
HttpResponse::new(413)
.with_json(&body)
.unwrap_or_else(|_| HttpResponse::new(413))
}
enum GuardRejection {
Reject,
Error(Error),
}
async fn evaluate_scoped_guards(
guards: &[ScopedGuard],
path: &str,
request: HttpRequest,
) -> Result<HttpRequest, GuardRejection> {
let matching: Vec<&ScopedGuard> = guards.iter().filter(|g| g.matches(path)).collect();
if matching.is_empty() {
return Ok(request);
}
let context = GuardContext::new(request);
for scoped in matching {
match scoped.guard.can_activate(&context).await {
Ok(true) => {}
Ok(false) => return Err(GuardRejection::Reject),
Err(err) => return Err(GuardRejection::Error(err)),
}
}
Ok(context.request)
}
pub(crate) mod response_wire {
use super::{CorsConfig, HttpResponse};
use crate::logging::{debug, warn};
pub(crate) fn name_is_token(name: &str) -> bool {
!name.is_empty()
&& name.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
pub(crate) fn value_is_emittable(value: &[u8]) -> bool {
!value.iter().any(|b| matches!(b, b'\r' | b'\n' | 0))
}
pub(crate) enum TransportField {
Framing,
ContentLength,
}
pub(crate) fn transport_field(name: &str) -> Option<TransportField> {
const FRAMING: [&str; 8] = [
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
];
if name.eq_ignore_ascii_case("content-length") {
return Some(TransportField::ContentLength);
}
FRAMING
.iter()
.any(|known| name.eq_ignore_ascii_case(known))
.then_some(TransportField::Framing)
}
pub(crate) fn report_transport_field(field: &TransportField, name: &str) {
match field {
TransportField::ContentLength => {
debug!(field = %name, "Dropping a handler-supplied Content-Length; the writer computes it");
}
TransportField::Framing => {
warn!(field = %name, "Dropping a hop-by-hop or framing header supplied by a handler");
}
}
}
pub(crate) fn report_unemittable(
name: &str,
value: &[u8],
method: &crate::Method,
path: &str,
what: &str,
) {
warn!(
method = %method,
path = %path,
what,
name_ok = name_is_token(name),
name_len = name.len(),
value_len = value.len(),
"Handler produced an unwritable header; failing the response closed"
);
}
pub(crate) fn internal_error_envelope() -> HttpResponse {
let body = serde_json::json!({
"error": "Internal Server Error",
"status": 500,
});
HttpResponse::new(500)
.with_json(&body)
.unwrap_or_else(|_| HttpResponse::new(500))
}
pub(crate) struct CorsAdditions {
pub(crate) origin: Option<String>,
pub(crate) credentials: bool,
pub(crate) vary_origin: bool,
}
pub(crate) fn cors_additions(handler_origin: Option<&str>, cors: &CorsConfig) -> CorsAdditions {
let effective_origin = handler_origin.unwrap_or(cors.allow_origin.as_str());
let authorised = match handler_origin {
None => true,
Some(origin) => origin.eq_ignore_ascii_case(&cors.allow_origin),
};
let credentials = if !cors.allow_credentials {
false
} else if !authorised {
warn!(
"Access-Control-Allow-Credentials withheld: the handler set an \
Access-Control-Allow-Origin the CORS configuration does not \
authorise, and credentials against an unvalidated origin would \
allow any site to read this response"
);
false
} else if effective_origin == "*" {
warn!(
"Access-Control-Allow-Credentials withheld: it is invalid \
alongside a wildcard origin, and a browser rejects the pair"
);
false
} else {
true
};
CorsAdditions {
origin: handler_origin.is_none().then(|| cors.allow_origin.clone()),
credentials,
vary_origin: handler_origin.is_some(),
}
}
}
fn to_hyper_response_raw(
response: HttpResponse,
method: &crate::Method,
path: &str,
) -> Response<Full<bytes::Bytes>> {
to_hyper_response(response, None, method, path)
}
fn to_hyper_response(
response: HttpResponse,
cors: Option<&CorsConfig>,
method: &crate::Method,
path: &str,
) -> Response<Full<bytes::Bytes>> {
use response_wire::{
cors_additions, name_is_token, report_transport_field, report_unemittable, transport_field,
value_is_emittable,
};
let mut builder = Response::builder().status(response.status);
for (key, value) in &response.headers {
if !name_is_token(key) || !value_is_emittable(value.as_bytes()) {
report_unemittable(key, value.as_bytes(), method, path, "header");
return unemittable_hyper_response(cors);
}
if let Some(field) = transport_field(key) {
report_transport_field(&field, key);
continue;
}
builder = builder.header(key, value);
}
for cookie_value in &response.cookies {
if !value_is_emittable(cookie_value.as_bytes()) {
report_unemittable(
"set-cookie",
cookie_value.as_bytes(),
method,
path,
"set-cookie",
);
return unemittable_hyper_response(cors);
}
builder = builder.header("Set-Cookie", cookie_value);
}
if let Some(cors) = cors {
let handler_origin = response
.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("access-control-allow-origin"))
.map(|(_, value)| value.as_str());
let additions = cors_additions(handler_origin, cors);
if let Some(origin) = &additions.origin {
builder = builder.header("Access-Control-Allow-Origin", origin);
}
if additions.credentials {
builder = builder.header("Access-Control-Allow-Credentials", "true");
}
if additions.vary_origin {
builder = builder.header("Vary", "Origin");
}
}
let body = Full::new(response.into_body_bytes());
builder.body(body).unwrap_or_else(|_| {
warn!(
method = %method,
path = %path,
"hyper refused a handler header; failing the response closed"
);
unemittable_hyper_response(cors)
})
}
fn unemittable_hyper_response(cors: Option<&CorsConfig>) -> Response<Full<bytes::Bytes>> {
let envelope = response_wire::internal_error_envelope();
let mut builder = Response::builder().status(envelope.status);
for (key, value) in &envelope.headers {
builder = builder.header(key, value);
}
if let Some(cors) = cors {
let additions = response_wire::cors_additions(None, cors);
if let Some(origin) = &additions.origin {
builder = builder.header("Access-Control-Allow-Origin", origin);
}
if additions.credentials {
builder = builder.header("Access-Control-Allow-Credentials", "true");
}
}
builder
.body(Full::new(envelope.into_body_bytes()))
.unwrap_or_else(|_| {
let mut fallback = Response::new(Full::new(bytes::Bytes::new()));
*fallback.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
fallback
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_socket_tuning_stores_config() {
let app = Application::new(Container::new(), Router::new())
.with_socket_tuning(EpollConfig::low_latency());
let config = app
.epoll_config
.as_ref()
.expect("with_socket_tuning should store the config");
assert_eq!(config.max_events, 256);
assert!(config.tcp_nodelay);
let plain = Application::new(Container::new(), Router::new());
assert!(plain.epoll_config.is_none());
}
#[test]
fn test_error_response_redacts_5xx_messages() {
let err = Error::Internal("db password auth failed for user 'app'".to_string());
let response = error_response(&err);
assert_eq!(response.status, 500);
let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
assert!(!body.contains("db password"));
assert!(body.contains("Internal Server Error"));
}
#[test]
fn test_error_response_keeps_4xx_messages() {
let err = Error::NotFound("User not found".to_string());
let response = error_response(&err);
assert_eq!(response.status, 404);
let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
assert!(body.contains("User not found"));
}
fn convert(response: HttpResponse, cors: Option<&CorsConfig>) -> Response<Full<bytes::Bytes>> {
to_hyper_response(response, cors, &crate::Method::Get, "/t")
}
#[test]
fn to_hyper_response_does_not_duplicate_a_handler_supplied_cors_origin() {
let mut response = HttpResponse::new(200);
response.headers.insert(
"Access-Control-Allow-Origin".to_string(),
"https://reflected.test".to_string(),
);
let cors = CorsConfig::new("https://configured.test");
let out = convert(response, Some(&cors));
let origins: Vec<_> = out
.headers()
.get_all("access-control-allow-origin")
.iter()
.map(|v| v.to_str().expect("ascii"))
.collect();
assert_eq!(
origins,
vec!["https://reflected.test"],
"the handler's reflected origin must stand alone; a second field \
makes the browser reject a response that was correct"
);
assert_eq!(
out.headers()
.get("vary")
.map(|v| v.to_str().expect("ascii")),
Some("Origin"),
"the origin came from the handler, so the response varies by it"
);
}
#[test]
fn to_hyper_response_withholds_credentials_from_a_wildcard_origin() {
let cors = CorsConfig::new("*").with_credentials();
let out = convert(HttpResponse::new(200), Some(&cors));
assert!(
out.headers()
.get("access-control-allow-credentials")
.is_none(),
"credentials must be withheld alongside a wildcard origin rather \
than emitted as a pair no browser will honour"
);
}
#[test]
fn to_hyper_response_withholds_credentials_from_an_unauthorised_reflected_origin() {
let mut response = HttpResponse::new(200);
response.headers.insert(
"Access-Control-Allow-Origin".to_string(),
"https://evil.test".to_string(),
);
let cors = CorsConfig::new("https://configured.test").with_credentials();
let out = convert(response, Some(&cors));
assert_eq!(
out.headers()
.get("access-control-allow-origin")
.map(|v| v.to_str().expect("ascii")),
Some("https://evil.test")
);
assert!(
out.headers()
.get("access-control-allow-credentials")
.is_none(),
"credentials belong only to an origin the configuration authorised"
);
}
#[test]
fn to_hyper_response_keeps_credentials_for_the_configured_origin() {
let mut response = HttpResponse::new(200);
response.headers.insert(
"Access-Control-Allow-Origin".to_string(),
"https://configured.test".to_string(),
);
let cors = CorsConfig::new("https://configured.test").with_credentials();
let out = convert(response, Some(&cors));
assert_eq!(
out.headers()
.get("access-control-allow-credentials")
.map(|v| v.to_str().expect("ascii")),
Some("true")
);
}
#[test]
fn to_hyper_response_drops_handler_supplied_framing_headers() {
let mut response = HttpResponse::new(413);
response
.headers
.insert("Connection".to_string(), "keep-alive".to_string());
response
.headers
.insert("Transfer-Encoding".to_string(), "chunked".to_string());
response
.headers
.insert("Upgrade".to_string(), "websocket".to_string());
response
.headers
.insert("Content-Length".to_string(), "999".to_string());
response
.headers
.insert("Content-Type".to_string(), "text/plain".to_string());
let out = convert(response, None);
for field in [
"connection",
"transfer-encoding",
"upgrade",
"content-length",
] {
assert!(
out.headers().get(field).is_none(),
"{field} is the transport's to decide, not the handler's — and \
a handler-supplied Content-Length over a body of another \
length is a desync a pooling proxy reads as the next response"
);
}
assert_eq!(
out.headers()
.get("content-type")
.map(|v| v.to_str().expect("ascii")),
Some("text/plain"),
"only the transport's own fields go; the rest survives"
);
}
#[test]
fn to_hyper_response_fails_a_response_closed_on_an_unwritable_field() {
for (name, value) in [
("X-Bad", "a\r\nx-injected: 1"),
("X-Bad", "a\nb"),
("X-Bad", "a\0b"),
("X Bad", "fine"),
("bad:name", "fine"),
] {
let mut response = HttpResponse::new(200);
response.headers.insert(
"Content-Security-Policy".to_string(),
"default-src 'none'".to_string(),
);
response.headers.insert(name.to_string(), value.to_string());
let out = convert(response, None);
assert_eq!(
out.status(),
500,
"{name}: {value:?} must fail the whole response closed"
);
assert!(out.headers().get("content-security-policy").is_none());
assert_eq!(
out.headers()
.get("content-type")
.map(|v| v.to_str().expect("ascii")),
Some("application/json"),
"an envelope, not a bare status: a credentialed fetch has to be \
able to tell a 500 from a network failure"
);
}
}
#[test]
fn the_hyper_fail_closed_500_carries_the_configured_cors_headers() {
let mut response = HttpResponse::new(200);
response
.headers
.insert("X Bad".to_string(), "fine".to_string());
let cors = CorsConfig::new("https://configured.test").with_credentials();
let out = convert(response, Some(&cors));
assert_eq!(out.status(), 500);
assert_eq!(
out.headers()
.get("access-control-allow-origin")
.map(|v| v.to_str().expect("ascii")),
Some("https://configured.test")
);
assert_eq!(
out.headers()
.get("access-control-allow-credentials")
.map(|v| v.to_str().expect("ascii")),
Some("true")
);
}
#[test]
fn a_preflight_withholds_credentials_from_a_wildcard_origin() {
let state = Application::new(Container::new(), Router::new())
.with_cors(CorsConfig::new("*").with_credentials())
.serve_state();
let preflight = cors_preflight(&crate::Method::Options, || true, &state)
.expect("a preflight answer is owed");
assert_eq!(preflight.status, 204);
assert!(
preflight
.headers
.get("Access-Control-Allow-Credentials")
.is_none(),
"the pair is invalid, so a browser discards the preflight entirely"
);
}
#[test]
fn a_preflight_keeps_credentials_for_a_named_origin() {
let state = Application::new(Container::new(), Router::new())
.with_cors(CorsConfig::new("https://configured.test").with_credentials())
.serve_state();
let preflight = cors_preflight(&crate::Method::Options, || true, &state)
.expect("a preflight answer is owed");
assert_eq!(
preflight
.headers
.get("Access-Control-Allow-Credentials")
.map(String::as_str),
Some("true")
);
}
#[test]
fn test_to_hyper_response_sets_headers_cookies_and_cors() {
let response = HttpResponse::ok()
.content_type("application/json")
.cookie("session", "abc; HttpOnly")
.with_body(b"{}".to_vec());
let cors = CorsConfig::new("https://example.com").with_credentials();
let hyper_resp = convert(response, Some(&cors));
assert_eq!(hyper_resp.status(), 200);
assert_eq!(
hyper_resp.headers().get("Content-Type").unwrap(),
"application/json"
);
assert_eq!(
hyper_resp.headers().get("Set-Cookie").unwrap(),
"session=abc; HttpOnly"
);
assert_eq!(
hyper_resp
.headers()
.get("Access-Control-Allow-Origin")
.unwrap(),
"https://example.com"
);
assert_eq!(
hyper_resp
.headers()
.get("Access-Control-Allow-Credentials")
.unwrap(),
"true"
);
}
#[test]
fn test_body_within_limit_boundary() {
assert!(body_within_limit(0, 10));
assert!(body_within_limit(10, 10));
assert!(!body_within_limit(11, 10));
let max = DEFAULT_MAX_BODY_SIZE;
assert!(body_within_limit(max, max));
assert!(!body_within_limit(max + 1, max));
}
#[test]
fn test_declared_content_length_accepts_a_comma_list() {
assert_eq!(declared_content_length(Some("100")), Some(100));
assert_eq!(declared_content_length(Some(" 100 ")), Some(100));
assert_eq!(declared_content_length(Some("100, 100")), Some(100));
assert_eq!(declared_content_length(Some("100,100")), Some(100));
assert_eq!(declared_content_length(None), None);
assert_eq!(declared_content_length(Some("")), None);
assert_eq!(declared_content_length(Some("banana")), None);
assert_eq!(declared_content_length(Some("-1")), None);
}
#[test]
fn test_without_h2_alpn_strips_only_h2() {
use rustls::ServerConfig;
let base =
ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("ring provider supports the default protocol versions")
.with_no_client_auth()
.with_cert_resolver(Arc::new(NoCertResolver));
let mut offering_h2 = base.clone();
offering_h2.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let stripped = without_h2_alpn(Arc::new(offering_h2));
assert_eq!(
stripped.alpn_protocols,
vec![b"http/1.1".to_vec()],
"a listener that closes h2 connections must not advertise h2"
);
let mut h1_only = base;
h1_only.alpn_protocols = vec![b"http/1.1".to_vec()];
let untouched = Arc::new(h1_only);
let same = without_h2_alpn(Arc::clone(&untouched));
assert!(Arc::ptr_eq(&untouched, &same));
}
#[derive(Debug)]
struct NoCertResolver;
impl rustls::server::ResolvesServerCert for NoCertResolver {
fn resolve(
&self,
_hello: rustls::server::ClientHello<'_>,
) -> Option<Arc<rustls::sign::CertifiedKey>> {
None
}
}
#[test]
fn test_payload_too_large_response_path() {
let resp = payload_too_large_response();
assert_eq!(resp.status, 413);
let body = String::from_utf8(resp.into_body_bytes().to_vec()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(parsed["status"], 413);
assert_eq!(parsed["error"], "Payload Too Large");
}
struct AllowGuard;
#[async_trait::async_trait]
impl Guard for AllowGuard {
async fn can_activate(&self, _ctx: &GuardContext) -> Result<bool, Error> {
Ok(true)
}
}
struct RecordingGuard {
ran: Arc<std::sync::atomic::AtomicBool>,
}
#[async_trait::async_trait]
impl Guard for RecordingGuard {
async fn can_activate(&self, _ctx: &GuardContext) -> Result<bool, Error> {
self.ran.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(true)
}
}
#[test]
fn test_scoped_guard_matches_is_segment_aware() {
let g = ScopedGuard {
prefix: "/admin".to_string(),
guard: Arc::new(AllowGuard),
};
assert!(g.matches("/admin"));
assert!(g.matches("/admin/users"));
assert!(!g.matches("/administrators"));
assert!(!g.matches("/public"));
let global = ScopedGuard {
prefix: String::new(),
guard: Arc::new(AllowGuard),
};
assert!(global.matches("/anything"));
assert!(global.matches("/"));
let root = ScopedGuard {
prefix: "/".to_string(),
guard: Arc::new(AllowGuard),
};
assert!(root.matches("/anything"));
}
#[tokio::test]
async fn test_scoped_guard_runs_only_for_its_controller_path() {
let ran = Arc::new(std::sync::atomic::AtomicBool::new(false));
let guards = vec![ScopedGuard {
prefix: "/admin".to_string(),
guard: Arc::new(RecordingGuard { ran: ran.clone() }),
}];
let req = HttpRequest::new("GET", "/admin/x".to_string());
let decision = evaluate_scoped_guards(&guards, "/admin/x", req).await;
assert!(decision.is_ok());
assert!(ran.load(std::sync::atomic::Ordering::SeqCst));
ran.store(false, std::sync::atomic::Ordering::SeqCst);
let req = HttpRequest::new("GET", "/public/y".to_string());
let decision = evaluate_scoped_guards(&guards, "/public/y", req).await;
assert!(decision.is_ok());
assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
ran.store(false, std::sync::atomic::Ordering::SeqCst);
let req = HttpRequest::new("GET", "/administrators".to_string());
let _ = evaluate_scoped_guards(&guards, "/administrators", req).await;
assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
}
fn admin_guard_registration() -> crate::module::GuardRegistration {
crate::module::GuardRegistration {
type_id: std::any::TypeId::of::<AllowGuard>(),
type_name: "AllowGuard",
factory: |_c| Ok(Arc::new(AllowGuard) as Arc<dyn Guard>),
}
}
fn controller_registration(base_path: &'static str) -> crate::ControllerRegistration {
crate::ControllerRegistration {
type_id: std::any::TypeId::of::<()>(),
type_name: "TestController",
base_path,
factory: |_c| Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>),
route_registrar: |_c, _r, _b| Ok(()),
}
}
struct AdminModule;
impl Module for AdminModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![controller_registration("/admin")]
}
fn guards(&self) -> Vec<crate::module::GuardRegistration> {
vec![admin_guard_registration()]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct GuardOnlyModule;
impl Module for GuardOnlyModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn guards(&self) -> Vec<crate::module::GuardRegistration> {
vec![admin_guard_registration()]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[test]
fn test_register_module_scopes_guard_to_controller_base_path() {
let container = Container::new();
let mut router = Router::new();
let mut guards: Vec<ScopedGuard> = Vec::new();
let mut visited = std::collections::HashSet::new();
Application::register_module(
&container,
&mut router,
&mut guards,
&mut visited,
&AdminModule,
);
assert_eq!(guards.len(), 1);
assert_eq!(guards[0].prefix, "/admin");
assert!(guards[0].matches("/admin/users"));
assert!(!guards[0].matches("/public"));
}
#[test]
fn test_register_module_guard_without_controllers_is_inert() {
let container = Container::new();
let mut router = Router::new();
let mut guards: Vec<ScopedGuard> = Vec::new();
let mut visited = std::collections::HashSet::new();
Application::register_module(
&container,
&mut router,
&mut guards,
&mut visited,
&GuardOnlyModule,
);
assert!(guards.is_empty());
}
struct DistinctProviderA;
struct DistinctProviderB;
async fn distinct_handler_a(
_req: crate::HttpRequest,
) -> Result<crate::HttpResponse, crate::Error> {
Ok(crate::HttpResponse::ok())
}
async fn distinct_handler_b(
_req: crate::HttpRequest,
) -> Result<crate::HttpResponse, crate::Error> {
Ok(crate::HttpResponse::ok())
}
fn distinct_controller_registration_a() -> crate::ControllerRegistration {
crate::ControllerRegistration {
type_id: std::any::TypeId::of::<()>(),
type_name: "DistinctControllerA",
base_path: "/distinct-a",
factory: |_c| Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>),
route_registrar: |_c, r, _b| {
r.get("/distinct-a", distinct_handler_a);
Ok(())
},
}
}
fn distinct_controller_registration_b() -> crate::ControllerRegistration {
crate::ControllerRegistration {
type_id: std::any::TypeId::of::<()>(),
type_name: "DistinctControllerB",
base_path: "/distinct-b",
factory: |_c| Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>),
route_registrar: |_c, r, _b| {
r.get("/distinct-b", distinct_handler_b);
Ok(())
},
}
}
struct DistinctModuleA;
impl Module for DistinctModuleA {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![crate::ProviderRegistration {
type_id: std::any::TypeId::of::<DistinctProviderA>(),
type_name: "DistinctProviderA",
register_fn: |c| c.register(DistinctProviderA),
}]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![distinct_controller_registration_a()]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct DistinctModuleB;
impl Module for DistinctModuleB {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![crate::ProviderRegistration {
type_id: std::any::TypeId::of::<DistinctProviderB>(),
type_name: "DistinctProviderB",
register_fn: |c| c.register(DistinctProviderB),
}]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![distinct_controller_registration_b()]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct DistinctRootModule;
impl Module for DistinctRootModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(DistinctModuleA), Box::new(DistinctModuleB)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[test]
fn test_register_module_registers_all_distinct_imported_modules() {
let container = Container::new();
let mut router = Router::new();
let mut guards: Vec<ScopedGuard> = Vec::new();
let mut visited = std::collections::HashSet::new();
Application::register_module(
&container,
&mut router,
&mut guards,
&mut visited,
&DistinctRootModule,
);
assert!(
container.has::<DistinctProviderA>(),
"first imported module's provider must be registered"
);
assert!(
container.has::<DistinctProviderB>(),
"second imported module's provider must be registered (must not \
be dropped as a false-positive duplicate of the first)"
);
assert!(
router.routes.iter().any(|r| r.path == "/distinct-a"),
"first imported module's controller route must be registered"
);
assert!(
router.routes.iter().any(|r| r.path == "/distinct-b"),
"second imported module's controller route must be registered"
);
}
struct SharedDiamondProvider;
static DIAMOND_PROVIDER_INIT_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
struct SharedDiamondModule;
impl Module for SharedDiamondModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![crate::ProviderRegistration {
type_id: std::any::TypeId::of::<SharedDiamondProvider>(),
type_name: "SharedDiamondProvider",
register_fn: |c| {
DIAMOND_PROVIDER_INIT_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
c.register(SharedDiamondProvider);
},
}]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct DiamondLeftModule;
impl Module for DiamondLeftModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(SharedDiamondModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct DiamondRightModule;
impl Module for DiamondRightModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(SharedDiamondModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct DiamondRootModule;
impl Module for DiamondRootModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(DiamondLeftModule), Box::new(DiamondRightModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[test]
fn test_register_module_diamond_import_registers_shared_module_once() {
let container = Container::new();
let mut router = Router::new();
let mut guards: Vec<ScopedGuard> = Vec::new();
let mut visited = std::collections::HashSet::new();
Application::register_module(
&container,
&mut router,
&mut guards,
&mut visited,
&DiamondRootModule,
);
assert!(
container.has::<SharedDiamondProvider>(),
"shared module reachable via a diamond must still register"
);
assert_eq!(
DIAMOND_PROVIDER_INIT_COUNT.load(std::sync::atomic::Ordering::SeqCst),
1,
"diamond-imported module (reached via two different parents) \
must register exactly once, not zero (dropped) or two \
(duplicated)"
);
}
static CREATE_DIAMOND_PROVIDER_INIT_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
struct CreateDiamondSharedProvider;
async fn create_diamond_shared_handler(
_req: crate::HttpRequest,
) -> Result<crate::HttpResponse, crate::Error> {
Ok(crate::HttpResponse::ok())
}
fn create_diamond_shared_controller_registration() -> crate::ControllerRegistration {
crate::ControllerRegistration {
type_id: std::any::TypeId::of::<()>(),
type_name: "CreateDiamondSharedController",
base_path: "/create-diamond-shared",
factory: |_c| Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>),
route_registrar: |_c, r, _b| {
r.get("/create-diamond-shared", create_diamond_shared_handler);
Ok(())
},
}
}
#[derive(Default)]
struct CreateDiamondSharedModule;
impl Module for CreateDiamondSharedModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![crate::ProviderRegistration {
type_id: std::any::TypeId::of::<CreateDiamondSharedProvider>(),
type_name: "CreateDiamondSharedProvider",
register_fn: |c| {
CREATE_DIAMOND_PROVIDER_INIT_COUNT
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
c.register(CreateDiamondSharedProvider);
},
}]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![create_diamond_shared_controller_registration()]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[derive(Default)]
struct CreateDiamondLeftModule;
impl Module for CreateDiamondLeftModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(CreateDiamondSharedModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[derive(Default)]
struct CreateDiamondRightModule;
impl Module for CreateDiamondRightModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(CreateDiamondSharedModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[derive(Default)]
struct CreateDiamondRootModule;
impl Module for CreateDiamondRootModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![
Box::new(CreateDiamondLeftModule),
Box::new(CreateDiamondRightModule),
]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[tokio::test]
async fn test_application_create_dedups_diamond_imported_module() {
CREATE_DIAMOND_PROVIDER_INIT_COUNT.store(0, std::sync::atomic::Ordering::SeqCst);
let app = Application::create::<CreateDiamondRootModule>().await;
assert!(
app.container.has::<CreateDiamondSharedProvider>(),
"shared module reachable via a diamond (through two different \
parent modules) must still register"
);
assert_eq!(
CREATE_DIAMOND_PROVIDER_INIT_COUNT.load(std::sync::atomic::Ordering::SeqCst),
1,
"diamond-imported module's provider must register exactly once \
through Application::create, not zero (dropped) or two \
(duplicated)"
);
let route_count = app
.router
.routes
.iter()
.filter(|r| r.path == "/create-diamond-shared")
.count();
assert_eq!(
route_count, 1,
"diamond-imported module's controller route must register \
exactly once through Application::create"
);
}
#[derive(Default)]
struct CyclicImportXModule;
impl Module for CyclicImportXModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(CyclicImportYModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
struct CyclicImportYModule;
impl Module for CyclicImportYModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(CyclicImportXModule)]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[tokio::test]
async fn test_application_create_terminates_on_cyclic_imports() {
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
Application::create::<CyclicImportXModule>(),
)
.await;
assert!(
result.is_ok(),
"Application::create must terminate for a cyclic module import \
graph, not hang"
);
}
#[test]
fn test_with_guard_registers_global_prefix() {
let app =
Application::new(Container::new(), Router::new()).with_guard(Arc::new(AllowGuard));
assert_eq!(app.guards.len(), 1);
assert!(app.guards[0].prefix.is_empty());
assert!(app.guards[0].matches("/any/path"));
}
static LIFECYCLE_PROBE_INIT_CALLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
static LIFECYCLE_PROBE_BOOTSTRAP_CALLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
static LIFECYCLE_PROBE_ORDER: std::sync::Mutex<Vec<&'static str>> =
std::sync::Mutex::new(Vec::new());
#[derive(Clone, Default)]
struct LifecycleProbeProvider;
#[async_trait::async_trait]
impl crate::lifecycle::OnModuleInit for LifecycleProbeProvider {
async fn on_module_init(&self) -> crate::lifecycle::LifecycleResult {
LIFECYCLE_PROBE_INIT_CALLED.store(true, std::sync::atomic::Ordering::SeqCst);
LIFECYCLE_PROBE_ORDER.lock().unwrap().push("init");
Ok(())
}
}
#[async_trait::async_trait]
impl crate::lifecycle::OnApplicationBootstrap for LifecycleProbeProvider {
async fn on_application_bootstrap(&self) -> crate::lifecycle::LifecycleResult {
LIFECYCLE_PROBE_BOOTSTRAP_CALLED.store(true, std::sync::atomic::Ordering::SeqCst);
LIFECYCLE_PROBE_ORDER.lock().unwrap().push("bootstrap");
Ok(())
}
}
#[derive(Default)]
struct LifecycleProbeModule;
impl Module for LifecycleProbeModule {
fn providers(&self) -> Vec<crate::ProviderRegistration> {
vec![crate::provider_registration!(
LifecycleProbeProvider,
LifecycleProbeProvider
)]
}
fn controllers(&self) -> Vec<crate::ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<std::any::TypeId> {
vec![]
}
}
#[tokio::test]
async fn test_application_create_fires_on_module_init_and_bootstrap_hooks() {
LIFECYCLE_PROBE_INIT_CALLED.store(false, std::sync::atomic::Ordering::SeqCst);
LIFECYCLE_PROBE_BOOTSTRAP_CALLED.store(false, std::sync::atomic::Ordering::SeqCst);
LIFECYCLE_PROBE_ORDER.lock().unwrap().clear();
let app = Application::create::<LifecycleProbeModule>().await;
assert!(
LIFECYCLE_PROBE_INIT_CALLED.load(std::sync::atomic::Ordering::SeqCst),
"OnModuleInit must fire automatically during Application::create"
);
assert!(
LIFECYCLE_PROBE_BOOTSTRAP_CALLED.load(std::sync::atomic::Ordering::SeqCst),
"OnApplicationBootstrap must fire automatically during Application::create"
);
assert!(app.container.has::<LifecycleProbeProvider>());
let order = LIFECYCLE_PROBE_ORDER.lock().unwrap().clone();
assert_eq!(
order,
vec!["init", "bootstrap"],
"OnModuleInit must run before OnApplicationBootstrap"
);
}
struct AlwaysNotFoundGuard;
#[async_trait::async_trait]
impl Guard for AlwaysNotFoundGuard {
async fn can_activate(&self, _ctx: &GuardContext) -> Result<bool, Error> {
Err(Error::NotFound("boom".to_string()))
}
}
struct RecordingCatchAllFilter {
called: Arc<std::sync::atomic::AtomicBool>,
}
#[async_trait::async_trait]
impl crate::exception_filter::ExceptionFilter for RecordingCatchAllFilter {
async fn catch(
&self,
error: &Error,
_ctx: &crate::exception_filter::ExceptionContext,
) -> Option<HttpResponse> {
if let Error::NotFound(_) = error {
self.called.store(true, std::sync::atomic::Ordering::SeqCst);
Some(
HttpResponse::new(599)
.with_json(&serde_json::json!({"caught_by": "RecordingCatchAllFilter"}))
.unwrap(),
)
} else {
None
}
}
}
#[test]
fn test_use_global_filter_populates_serve_state() {
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let app = Application::new(Container::new(), Router::new()).use_global_filter(
RecordingCatchAllFilter {
called: called.clone(),
},
);
assert!(app.filter_chain.is_some());
let state = app.serve_state();
assert!(
state.filter_chain.is_some(),
"serve_state must carry the configured filter chain through to ServeState"
);
}
#[test]
fn test_no_filter_configured_leaves_serve_state_filter_chain_none() {
let app = Application::new(Container::new(), Router::new());
let state = app.serve_state();
assert!(
state.filter_chain.is_none(),
"without use_global_filter, ServeState must carry no filter chain, \
preserving the original error_response fallback behavior"
);
}
#[tokio::test]
async fn test_use_global_filter_transforms_error_in_live_handle_request() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let filter_chain = Arc::new(
crate::exception_filter::ExceptionFilterChain::new().add_filter(
RecordingCatchAllFilter {
called: called.clone(),
},
),
);
let state = ServeState {
router: Arc::new(OptimizedRouter::from_router(&Router::new())),
cors: None,
guards: vec![ScopedGuard {
prefix: String::new(),
guard: Arc::new(AlwaysNotFoundGuard),
}]
.into(),
max_body_size: DEFAULT_MAX_BODY_SIZE,
filter_chain: Some(filter_chain),
peer: None,
};
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let io = TokioIo::new(stream);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
async move { handle_request(req, state).await }
});
let _ = http1::Builder::new().serve_connection(io, service).await;
});
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
stream
.write_all(b"GET /anything HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut raw_response = Vec::new();
let _ = tokio::time::timeout(
std::time::Duration::from_secs(5),
stream.read_to_end(&mut raw_response),
)
.await;
let raw_response = String::from_utf8_lossy(&raw_response);
assert!(
raw_response.starts_with("HTTP/1.1 599"),
"expected the filter's custom 599 status, got: {raw_response}"
);
assert!(
raw_response.contains("RecordingCatchAllFilter"),
"expected the filter's custom body, got: {raw_response}"
);
assert!(
called.load(std::sync::atomic::Ordering::SeqCst),
"the registered filter's catch() must actually have run"
);
}
async fn always_erroring_handler(_req: HttpRequest) -> Result<HttpResponse, Error> {
Err(Error::NotFound("handler boom".to_string()))
}
#[tokio::test]
async fn test_use_global_filter_transforms_handler_error_in_live_handle_request() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let filter_chain = Arc::new(
crate::exception_filter::ExceptionFilterChain::new().add_filter(
RecordingCatchAllFilter {
called: called.clone(),
},
),
);
let mut router = Router::new();
router.get("/broken", always_erroring_handler);
let state = ServeState {
router: Arc::new(OptimizedRouter::from_router(&router)),
cors: None,
guards: Vec::new().into(),
max_body_size: DEFAULT_MAX_BODY_SIZE,
filter_chain: Some(filter_chain),
peer: None,
};
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let io = TokioIo::new(stream);
let service = service_fn(move |req: Request<IncomingBody>| {
let state = state.clone();
async move { handle_request(req, state).await }
});
let _ = http1::Builder::new().serve_connection(io, service).await;
});
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
stream
.write_all(b"GET /broken HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut raw_response = Vec::new();
let _ = tokio::time::timeout(
std::time::Duration::from_secs(5),
stream.read_to_end(&mut raw_response),
)
.await;
let raw_response = String::from_utf8_lossy(&raw_response);
assert!(
raw_response.starts_with("HTTP/1.1 599"),
"expected the filter's custom 599 status, got: {raw_response}"
);
assert!(
raw_response.contains("RecordingCatchAllFilter"),
"expected the filter's custom body, got: {raw_response}"
);
assert!(
called.load(std::sync::atomic::Ordering::SeqCst),
"the registered filter's catch() must actually have run for a \
real handler error, not just a guard rejection"
);
}
struct PanickingFilter;
#[async_trait::async_trait]
impl crate::exception_filter::ExceptionFilter for PanickingFilter {
async fn catch(
&self,
_error: &Error,
_ctx: &crate::exception_filter::ExceptionContext,
) -> Option<HttpResponse> {
panic!("PanickingFilter deliberately panics for test coverage");
}
}
struct HangingFilter;
#[async_trait::async_trait]
impl crate::exception_filter::ExceptionFilter for HangingFilter {
async fn catch(
&self,
_error: &Error,
_ctx: &crate::exception_filter::ExceptionContext,
) -> Option<HttpResponse> {
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
None
}
}
#[tokio::test]
async fn test_respond_to_error_falls_back_when_filter_panics() {
let chain = Arc::new(
crate::exception_filter::ExceptionFilterChain::new().add_filter(PanickingFilter),
);
let req = HttpRequest::new("GET", "/panics".to_string());
let err = Error::Internal("boom".to_string());
let response = respond_to_error_with_timeout(
err,
Some(req),
Some(chain),
std::time::Duration::from_secs(5),
)
.await;
assert_eq!(response.status, 500);
let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
assert!(
body.contains("Internal Server Error"),
"a panicking filter must fall back to the redacted default 5xx \
body, got: {body}"
);
}
#[tokio::test]
async fn test_respond_to_error_falls_back_when_filter_hangs() {
let chain = Arc::new(
crate::exception_filter::ExceptionFilterChain::new().add_filter(HangingFilter),
);
let req = HttpRequest::new("GET", "/hangs".to_string());
let err = Error::Internal("boom".to_string());
let start = std::time::Instant::now();
let response = respond_to_error_with_timeout(
err,
Some(req),
Some(chain),
std::time::Duration::from_millis(50),
)
.await;
let elapsed = start.elapsed();
assert_eq!(response.status, 500);
let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap();
assert!(
body.contains("Internal Server Error"),
"a hanging filter must fall back to the redacted default 5xx \
body, got: {body}"
);
assert!(
elapsed < std::time::Duration::from_secs(2),
"a hanging filter must not block the caller past the configured \
timeout, took {elapsed:?}"
);
}
}