use crate::handler::{BoxedHandler, IntoHandler};
use crate::route_cache::OptimizedRouter;
use crate::{DEFAULT_MAX_BODY_SIZE, Error, HttpMethod, HttpRequest, HttpResponse, Router};
use bytes::Bytes;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::future::Future;
use std::net::ToSocketAddrs;
use std::pin::Pin;
use std::sync::Arc;
pub use crate::{HttpRequest as Request, HttpResponse as Response};
#[derive(Clone)]
pub struct Data<T: Clone + Send + Sync + 'static>(Arc<T>);
impl<T: Clone + Send + Sync + 'static> Data<T> {
pub fn new(data: T) -> Self {
Self(Arc::new(data))
}
pub fn get_ref(&self) -> &T {
&self.0
}
pub fn into_inner(self) -> Arc<T> {
self.0
}
}
impl<T: Clone + Send + Sync + 'static> std::ops::Deref for Data<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct App {
router: Router,
middleware: Vec<Arc<dyn Middleware>>,
state: AppState,
default_service: Option<BoxedHandler>,
max_body_size: usize,
}
#[derive(Default, Clone)]
struct AppState {
data: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}
impl AppState {
fn insert<T: Clone + Send + Sync + 'static>(&mut self, data: T) {
self.data.insert(TypeId::of::<T>(), Arc::new(data));
}
#[allow(dead_code)]
pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<Data<T>> {
self.data
.get(&TypeId::of::<T>())
.and_then(|arc| arc.downcast_ref::<T>())
.map(|t| Data(Arc::new(t.clone())))
}
}
impl App {
pub fn new() -> Self {
Self {
router: Router::new(),
middleware: Vec::new(),
state: AppState::default(),
default_service: None,
max_body_size: DEFAULT_MAX_BODY_SIZE,
}
}
pub fn data<T: Clone + Send + Sync + 'static>(mut self, data: T) -> Self {
self.state.insert(data);
self
}
pub fn wrap<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.middleware.push(Arc::new(middleware));
self
}
pub fn route(mut self, path: &str, route: RouteBuilder) -> Self {
for (method, handler) in route.handlers {
self.router.add_route(crate::routing::Route {
method,
path: path.to_string(),
handler,
constraints: None,
});
}
self
}
pub fn service(mut self, scope: Scope) -> Self {
for route in scope.into_routes() {
self.router.add_route(crate::routing::Route {
method: route.method,
path: route.path,
handler: route.handler,
constraints: route.constraints,
});
}
self
}
pub fn default_service<H, Args>(mut self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.default_service = Some(BoxedHandler::new(handler.into_handler()));
self
}
pub fn with_max_body_size(mut self, bytes: usize) -> Self {
self.max_body_size = bytes;
self
}
pub async fn run(self, addr: impl ToSocketAddrs) -> std::io::Result<()> {
let addr = addr.to_socket_addrs()?.next().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid address")
})?;
let app = Arc::new(BuiltApp {
router: Arc::new(OptimizedRouter::from_router(&self.router)),
middleware: self.middleware,
state: self.state,
default_service: self.default_service,
max_body_size: self.max_body_size,
});
run_server(app, addr).await
}
pub fn build(self) -> BuiltApp {
BuiltApp {
router: Arc::new(OptimizedRouter::from_router(&self.router)),
middleware: self.middleware,
state: self.state,
default_service: self.default_service,
max_body_size: self.max_body_size,
}
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
pub struct BuiltApp {
router: Arc<OptimizedRouter>,
middleware: Vec<Arc<dyn Middleware>>,
state: AppState,
default_service: Option<BoxedHandler>,
max_body_size: usize,
}
impl BuiltApp {
pub async fn handle(&self, mut request: HttpRequest) -> Result<HttpResponse, Error> {
request.extensions.insert(self.state.clone());
let router = self.router.clone();
let default_service = self.default_service.clone();
let handler: Next = Box::new(move |req| {
let router = router.clone();
let default_service = default_service.clone();
Box::pin(async move {
match router.route(req).await {
Ok(response) => Ok(response),
Err(Error::RouteNotFound(_)) if default_service.is_some() => {
let req = HttpRequest::new("GET", "/404".to_string());
default_service.unwrap().call(req).await
}
Err(e) => Err(e),
}
})
});
let mut next = handler;
for mw in self.middleware.iter().rev() {
let mw = mw.clone();
next = Box::new(move |req| mw.call(req, next));
}
next(request).await
}
}
pub struct RouteBuilder {
handlers: Vec<(HttpMethod, BoxedHandler)>,
}
impl RouteBuilder {
fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
fn with_method<H, Args>(mut self, method: HttpMethod, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.handlers
.push((method, BoxedHandler::new(handler.into_handler())));
self
}
pub fn get<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::GET, handler)
}
pub fn post<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::POST, handler)
}
pub fn put<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::PUT, handler)
}
pub fn delete<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::DELETE, handler)
}
pub fn patch<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::PATCH, handler)
}
pub fn head<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::HEAD, handler)
}
pub fn options<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::OPTIONS, handler)
}
pub fn query<H, Args>(self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.with_method(HttpMethod::QUERY, handler)
}
}
pub fn get<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().get(handler)
}
pub fn post<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().post(handler)
}
pub fn put<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().put(handler)
}
pub fn delete<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().delete(handler)
}
pub fn patch<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().patch(handler)
}
pub fn head<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().head(handler)
}
pub fn options<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().options(handler)
}
pub fn query<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args>,
{
RouteBuilder::new().query(handler)
}
pub fn any<H, Args>(handler: H) -> RouteBuilder
where
H: IntoHandler<Args> + Clone,
{
RouteBuilder::new()
.get(handler.clone())
.post(handler.clone())
.put(handler.clone())
.delete(handler.clone())
.patch(handler.clone())
.head(handler.clone())
.options(handler)
}
pub struct Scope {
prefix: String,
routes: Vec<ScopeRoute>,
middleware: Vec<Arc<dyn Middleware>>,
}
struct ScopeRoute {
method: HttpMethod,
path: String,
handler: BoxedHandler,
constraints: Option<crate::route_constraint::RouteConstraints>,
}
impl Scope {
fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
routes: Vec::new(),
middleware: Vec::new(),
}
}
pub fn route(mut self, path: &str, route: RouteBuilder) -> Self {
for (method, handler) in route.handlers {
self.routes.push(ScopeRoute {
method,
path: path.to_string(),
handler,
constraints: None,
});
}
self
}
pub fn wrap<M: Middleware + 'static>(mut self, middleware: M) -> Self {
self.middleware.push(Arc::new(middleware));
self
}
pub fn service(mut self, inner: Scope) -> Self {
self.routes.extend(inner.into_routes());
self
}
fn into_routes(self) -> Vec<ScopeRoute> {
let Self {
prefix,
routes,
middleware,
} = self;
routes
.into_iter()
.map(|route| ScopeRoute {
method: route.method,
path: format!("{}{}", prefix, route.path),
handler: wrap_handler(route.handler, &middleware),
constraints: route.constraints,
})
.collect()
}
}
fn wrap_handler(handler: BoxedHandler, middleware: &[Arc<dyn Middleware>]) -> BoxedHandler {
let mut handler = handler;
for mw in middleware.iter().rev() {
let mw = Arc::clone(mw);
let inner = handler;
handler = BoxedHandler::new(
(move |req: HttpRequest| {
let mw = Arc::clone(&mw);
let inner = inner.clone();
async move {
let next: Next = Box::new(move |req| inner.call(req));
mw.call(req, next).await
}
})
.into_handler(),
);
}
handler
}
pub fn scope(prefix: impl Into<String>) -> Scope {
Scope::new(prefix)
}
pub type Next = Box<
dyn FnOnce(HttpRequest) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>
+ Send,
>;
pub trait Middleware: Send + Sync {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>;
}
pub struct Logger {
#[allow(dead_code)]
format: LogFormat,
}
#[derive(Clone, Copy, Default)]
pub enum LogFormat {
#[default]
Default,
Combined,
Short,
}
impl Default for Logger {
fn default() -> Self {
Self {
format: LogFormat::Default,
}
}
}
impl Logger {
pub fn new(format: LogFormat) -> Self {
Self { format }
}
}
impl Middleware for Logger {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>> {
let method = req.method.clone();
let path = req.path.clone();
Box::pin(async move {
let start = std::time::Instant::now();
let result = next(req).await;
let elapsed = start.elapsed();
match &result {
Ok(response) => {
tracing::info!(
method = %method,
path = %path,
status = response.status,
duration_ms = elapsed.as_millis() as u64,
"Request completed"
);
}
Err(e) => {
tracing::error!(
method = %method,
path = %path,
error = %e,
duration_ms = elapsed.as_millis() as u64,
"Request failed"
);
}
}
result
})
}
}
pub struct Cors {
allowed_origins: Vec<String>,
allowed_methods: Vec<String>,
allowed_headers: Vec<String>,
allow_credentials: bool,
max_age: u32,
}
impl Default for Cors {
fn default() -> Self {
Self {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"OPTIONS".to_string(),
],
allowed_headers: vec!["*".to_string()],
allow_credentials: false,
max_age: 86400,
}
}
}
impl Cors {
pub fn permissive() -> Self {
Self::default()
}
pub fn allowed_origins(mut self, origins: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.allowed_origins = origins.into_iter().map(Into::into).collect();
self
}
pub fn allowed_methods(mut self, methods: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.allowed_methods = methods.into_iter().map(Into::into).collect();
self
}
pub fn allowed_headers(mut self, headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.allowed_headers = headers.into_iter().map(Into::into).collect();
self
}
pub fn allow_credentials(mut self, allow: bool) -> Self {
self.allow_credentials = allow;
self
}
pub fn max_age(mut self, seconds: u32) -> Self {
self.max_age = seconds;
self
}
}
impl Middleware for Cors {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>> {
let is_preflight = req.method == "OPTIONS";
let allowed_origins = self.allowed_origins.clone();
let allowed_methods = self.allowed_methods.join(", ");
let allowed_headers = self.allowed_headers.join(", ");
let allow_credentials = self.allow_credentials;
let max_age = self.max_age;
let allowed_origin_for = move |origin: &str| -> Option<String> {
if allowed_origins.iter().any(|o| o == "*") {
if allow_credentials {
Some(origin.to_string())
} else {
Some("*".to_string())
}
} else if allowed_origins.iter().any(|o| o == origin) {
Some(origin.to_string())
} else {
None
}
};
Box::pin(async move {
let request_origin = req.headers.get("origin").map(str::to_owned);
if is_preflight {
let mut response = HttpResponse::no_content();
if let Some(origin) = request_origin.as_deref()
&& let Some(allow_origin) = allowed_origin_for(origin)
{
response
.headers
.insert("Access-Control-Allow-Origin".to_string(), allow_origin);
}
response
.headers
.insert("Access-Control-Allow-Methods".to_string(), allowed_methods);
response
.headers
.insert("Access-Control-Allow-Headers".to_string(), allowed_headers);
response
.headers
.insert("Access-Control-Max-Age".to_string(), max_age.to_string());
if allow_credentials {
response.headers.insert(
"Access-Control-Allow-Credentials".to_string(),
"true".to_string(),
);
}
return Ok(response);
}
let mut response = next(req).await?;
if let Some(origin) = request_origin.as_deref()
&& let Some(allow_origin) = allowed_origin_for(origin)
{
response
.headers
.insert("Access-Control-Allow-Origin".to_string(), allow_origin);
}
if allow_credentials {
response.headers.insert(
"Access-Control-Allow-Credentials".to_string(),
"true".to_string(),
);
}
Ok(response)
})
}
}
pub struct Compress {
level: CompressionLevel,
}
#[derive(Clone, Copy, Default)]
pub enum CompressionLevel {
Fast,
#[default]
Default,
Best,
}
impl CompressionLevel {
fn to_flate2(self) -> flate2::Compression {
match self {
CompressionLevel::Fast => flate2::Compression::fast(),
CompressionLevel::Default => flate2::Compression::default(),
CompressionLevel::Best => flate2::Compression::best(),
}
}
}
pub(crate) fn accepts_gzip(req: &HttpRequest) -> bool {
let Some(value) = req.headers.get("accept-encoding") else {
return false;
};
value.split(',').any(|part| {
let mut segs = part.split(';').map(str::trim);
let coding = segs.next().unwrap_or("");
if !coding.eq_ignore_ascii_case("gzip") && coding != "*" {
return false;
}
!segs.any(|p| {
p.split_once('=')
.filter(|(k, _)| k.eq_ignore_ascii_case("q"))
.and_then(|(_, v)| v.parse::<f32>().ok())
.map(|q| q == 0.0)
.unwrap_or(false)
})
})
}
pub(crate) fn gzip_encode(data: &[u8], level: CompressionLevel) -> Option<Vec<u8>> {
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::with_capacity(data.len() / 2 + 32), level.to_flate2());
encoder.write_all(data).ok()?;
encoder.finish().ok()
}
pub(crate) const GZIP_OFFLOAD_THRESHOLD: usize = 32 * 1024;
pub(crate) async fn gzip_encode_offloaded(
data: Vec<u8>,
level: CompressionLevel,
) -> Result<Vec<u8>, Vec<u8>> {
let level_label = match level {
CompressionLevel::Fast => "fast",
CompressionLevel::Default => "default",
CompressionLevel::Best => "best",
};
if data.len() < GZIP_OFFLOAD_THRESHOLD {
let body_len = data.len();
return match gzip_encode(&data, level) {
Some(compressed) => Ok(compressed),
None => {
tracing::warn!(
body_len,
level = level_label,
"gzip encode failed; serving response uncompressed"
);
Err(data)
}
};
}
let body_len = data.len();
let shared = Arc::new(data);
let for_task = Arc::clone(&shared);
match tokio::task::spawn_blocking(move || gzip_encode(&for_task, level)).await {
Ok(Some(compressed)) => Ok(compressed),
Ok(None) => {
tracing::warn!(
body_len,
level = level_label,
"gzip encode failed on offload path; serving response uncompressed"
);
Err(Arc::try_unwrap(shared).unwrap_or_else(|shared| (*shared).clone()))
}
Err(join_err) => {
tracing::warn!(
body_len,
level = level_label,
error = %join_err,
"gzip offload task did not complete normally (panicked or was cancelled); \
serving response uncompressed"
);
Err(Arc::try_unwrap(shared).unwrap_or_else(|shared| (*shared).clone()))
}
}
}
pub(crate) async fn apply_gzip_offload(
mut response: HttpResponse,
level: CompressionLevel,
) -> HttpResponse {
let had_content_length = response.headers.contains_key("Content-Length");
let original: Vec<u8> = response.body.to_vec();
match gzip_encode_offloaded(original, level).await {
Ok(compressed) => {
response = response.with_body(compressed);
response
.headers
.insert("Content-Encoding".to_string(), "gzip".to_string());
if had_content_length {
response.headers.insert(
"Content-Length".to_string(),
response.body_len().to_string(),
);
}
}
Err(_original) => {
}
}
response
}
pub(crate) fn add_vary_accept_encoding(response: &mut HttpResponse) {
const TOKEN: &str = "Accept-Encoding";
let merged = match response.headers.get("Vary") {
None => TOKEN.to_string(),
Some(existing) => {
let already_present = existing.trim() == "*"
|| existing
.split(',')
.any(|part| part.trim().eq_ignore_ascii_case(TOKEN));
if already_present {
return;
}
format!("{}, {}", existing, TOKEN)
}
};
response.headers.insert("Vary".to_string(), merged);
}
impl Default for Compress {
fn default() -> Self {
Self {
level: CompressionLevel::Default,
}
}
}
impl Compress {
pub fn new(level: CompressionLevel) -> Self {
Self { level }
}
}
impl Middleware for Compress {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>> {
let level = self.level;
let client_accepts_gzip = accepts_gzip(&req);
Box::pin(async move {
let mut response = next(req).await?;
add_vary_accept_encoding(&mut response);
let already_encoded = response.headers.contains_key("Content-Encoding");
if client_accepts_gzip && !already_encoded && !response.body_ref().is_empty() {
response = apply_gzip_offload(response, level).await;
}
Ok(response)
})
}
}
fn error_response_parts(e: &Error) -> (u16, String) {
let status = e.status_code();
if status >= 500 {
tracing::error!(error = %e, status, "Request handler failed");
}
let response = e.to_client_response();
let body = String::from_utf8(response.into_body_bytes().to_vec()).unwrap_or_default();
(status, body)
}
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))
}
fn to_hyper_response(resp: HttpResponse) -> hyper::Response<http_body_util::Full<bytes::Bytes>> {
let mut builder = hyper::Response::builder().status(resp.status);
for (name, value) in &resp.headers {
builder = builder.header(name.as_str(), value.as_str());
}
for cookie in &resp.cookies {
builder = builder.header("Set-Cookie", cookie.as_str());
}
builder
.body(http_body_util::Full::new(resp.into_body_bytes()))
.unwrap()
}
async fn handle_micro_request(
req: hyper::Request<hyper::body::Incoming>,
app: Arc<BuiltApp>,
peer: Option<std::net::SocketAddr>,
) -> Result<hyper::Response<http_body_util::Full<bytes::Bytes>>, std::convert::Infallible> {
use http_body_util::{BodyExt, Limited};
let method = crate::Method::from(req.method().as_str());
let path = req
.uri()
.path_and_query()
.map(|pq| pq.to_string())
.unwrap_or_else(|| "/".to_string());
let mut http_req = HttpRequest::new(method.clone(), path.clone()).with_peer(peer);
for (name, value) in req.headers() {
http_req
.headers
.insert(name.as_str(), Bytes::copy_from_slice(value.as_bytes()));
}
if let Some(declared_len) = http_req
.headers
.get("content-length")
.and_then(|v| v.parse::<usize>().ok())
&& declared_len > app.max_body_size
{
tracing::warn!(
method = %method,
path = %path,
limit = app.max_body_size,
declared_len,
"Request Content-Length exceeds configured limit"
);
return Ok(to_hyper_response(payload_too_large_response()));
}
let limited = Limited::new(req.into_body(), app.max_body_size);
let body_bytes = match limited.collect().await {
Ok(collected) => collected.to_bytes(),
Err(err) if err.is::<http_body_util::LengthLimitError>() => {
tracing::warn!(
method = %method,
path = %path,
limit = app.max_body_size,
"Request body exceeds configured limit"
);
return Ok(to_hyper_response(payload_too_large_response()));
}
Err(err) => {
tracing::warn!(method = %method, path = %path, error = %err, "Failed to read request body");
return Ok(to_hyper_response(HttpResponse::new(400)));
}
};
http_req.body = body_bytes;
let response = app.handle(http_req).await;
match response {
Ok(resp) => Ok(to_hyper_response(resp)),
Err(e) => {
let (status, body) = error_response_parts(&e);
Ok(hyper::Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(http_body_util::Full::new(bytes::Bytes::from(body)))
.unwrap())
}
}
}
async fn serve(listener: tokio::net::TcpListener, app: Arc<BuiltApp>) -> std::io::Result<()> {
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
loop {
let (stream, client_addr) = listener.accept().await?;
let io = TokioIo::new(stream);
let app = app.clone();
tokio::spawn(async move {
let service = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
handle_micro_request(req, app.clone(), Some(client_addr))
});
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
tracing::error!("Connection error: {}", err);
}
});
}
}
async fn run_server(app: Arc<BuiltApp>, addr: std::net::SocketAddr) -> std::io::Result<()> {
use tokio::net::TcpListener;
let listener = TcpListener::bind(addr).await?;
tracing::info!("Micro-framework server listening on http://{}", addr);
serve(listener, app).await
}
#[cfg(test)]
mod tests {
use super::*;
async fn test_handler(_req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
#[test]
fn test_app_builder() {
let app = App::new()
.route("/", get(test_handler))
.route("/users", get(test_handler).post(test_handler))
.build();
assert_eq!(app.router.len(), 3);
}
#[test]
fn test_scope() {
let scope = scope("/api")
.route("/users", get(test_handler))
.route("/posts", get(test_handler).post(test_handler));
assert_eq!(scope.routes.len(), 3);
}
#[test]
fn test_data() {
let data = Data::new(42i32);
assert_eq!(*data, 42);
}
#[tokio::test]
async fn test_built_app_handle() {
let app = App::new().route("/test", get(test_handler)).build();
let req = HttpRequest::new("GET", "/test".to_string());
let response = app.handle(req).await.unwrap();
assert_eq!(response.status, 200);
}
struct CountingMiddleware {
calls: Arc<std::sync::atomic::AtomicUsize>,
}
impl Middleware for CountingMiddleware {
fn call(
&self,
req: HttpRequest,
next: Next,
) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>> {
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
next(req)
}
}
#[tokio::test]
async fn test_scope_middleware_runs() {
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let app = App::new()
.service(
scope("/admin")
.wrap(CountingMiddleware {
calls: calls.clone(),
})
.route("/users", get(test_handler)),
)
.route("/public", get(test_handler))
.build();
let req = HttpRequest::new("GET", "/admin/users".to_string());
let response = app.handle(req).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
let req = HttpRequest::new("GET", "/public".to_string());
let response = app.handle(req).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_nested_scope_inherits_parent_middleware() {
let parent_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let inner_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let app = App::new()
.service(
scope("/api")
.wrap(CountingMiddleware {
calls: parent_calls.clone(),
})
.service(
scope("/v1")
.wrap(CountingMiddleware {
calls: inner_calls.clone(),
})
.route("/users", get(test_handler)),
),
)
.build();
let req = HttpRequest::new("GET", "/api/v1/users".to_string());
let response = app.handle(req).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(parent_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(inner_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_built_app_dispatches_catch_all() {
async fn files(req: HttpRequest) -> Result<HttpResponse, Error> {
let p = req.param("path").map(str::to_owned).unwrap_or_default();
Ok(HttpResponse::ok().with_body(p.into_bytes()))
}
let app = App::new().route("/files/*path", get(files)).build();
let req = HttpRequest::new("GET", "/files/docs/readme.md".to_string());
let resp = app.handle(req).await.unwrap();
assert_eq!(resp.body, Bytes::from_static(b"docs/readme.md"));
}
#[tokio::test]
async fn test_built_app_query_method_and_unknown_method() {
async fn echo(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok().with_bytes_body(req.body.clone()))
}
let app = App::new().route("/search", query(echo)).build();
let mut req = HttpRequest::new("QUERY", "/search".to_string());
req.body = Bytes::from_static(b"name=john");
let resp = app.handle(req).await.unwrap();
assert_eq!(resp.into_body_bytes().as_ref(), b"name=john");
let app2 = App::new().route("/search", get(echo)).build();
let req = HttpRequest::new("PROPFIND", "/search".to_string());
let err = app2.handle(req).await;
assert!(matches!(err, Err(Error::RouteNotFound(_))));
}
#[test]
fn test_error_response_parts_uses_status_code() {
assert_eq!(error_response_parts(&Error::Conflict("dup".into())).0, 409);
assert_eq!(
error_response_parts(&Error::TooManyRequests("slow down".into())).0,
429
);
assert_eq!(error_response_parts(&Error::NotFound("gone".into())).0, 404);
assert_eq!(
error_response_parts(&Error::ServiceUnavailable("down".into())).0,
503
);
}
#[test]
fn test_error_response_parts_escapes_json() {
let e = Error::Validation(r#"bad "quoted" input"#.to_string());
let (status, body) = error_response_parts(&e);
assert_eq!(status, 400);
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(
parsed["error"]
.as_str()
.unwrap()
.contains(r#"bad "quoted" input"#)
);
}
#[test]
fn test_error_response_parts_hides_internal_message() {
let e = Error::Internal("secret database password".to_string());
let (status, body) = error_response_parts(&e);
assert_eq!(status, 500);
assert!(!body.contains("secret database password"));
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(parsed["error"], "Internal Server Error");
}
fn body_next(body: Vec<u8>) -> Next {
Box::new(move |_req: HttpRequest| {
Box::pin(async move {
let mut resp = HttpResponse::ok();
resp.body = Bytes::from(body);
Ok(resp)
})
})
}
fn gunzip(data: &[u8]) -> Vec<u8> {
use std::io::Read;
let mut decoder = flate2::read::GzDecoder::new(data);
let mut out = Vec::new();
decoder.read_to_end(&mut out).expect("valid gzip stream");
out
}
#[tokio::test]
async fn test_compress_gzips_when_accepted() {
let mw = Compress::new(CompressionLevel::Best);
let mut req = HttpRequest::new("GET", "/");
req.headers.insert("accept-encoding", "gzip, deflate, br");
let original = vec![b'a'; 8192];
let resp = mw.call(req, body_next(original.clone())).await.unwrap();
assert_eq!(
resp.headers.get("Content-Encoding").map(String::as_str),
Some("gzip")
);
assert_eq!(
resp.headers.get("Vary").map(String::as_str),
Some("Accept-Encoding")
);
assert!(resp.body_ref().len() < original.len());
assert_eq!(gunzip(resp.body_ref()), original);
}
#[tokio::test]
async fn test_compress_skips_without_accept_encoding() {
let mw = Compress::default();
let req = HttpRequest::new("GET", "/");
let original = vec![b'b'; 8192];
let resp = mw.call(req, body_next(original.clone())).await.unwrap();
assert!(
resp.headers
.get("Content-Encoding")
.map(String::as_str)
.is_none()
);
assert_eq!(resp.body_ref(), original.as_slice());
assert_eq!(
resp.headers.get("Vary").map(String::as_str),
Some("Accept-Encoding")
);
}
#[tokio::test]
async fn test_compress_merges_vary_with_existing_value() {
let mw = Compress::new(CompressionLevel::Best);
let mut req = HttpRequest::new("GET", "/");
req.headers.insert("accept-encoding", "gzip");
let original = vec![b'c'; 8192];
let next: Next = Box::new(move |_req: HttpRequest| {
Box::pin(async move {
let mut resp = HttpResponse::ok();
resp.body = Bytes::from(original);
resp.headers
.insert("Vary".to_string(), "Origin".to_string());
Ok(resp)
})
});
let resp = mw.call(req, next).await.unwrap();
let vary = resp.headers.get("Vary").cloned().unwrap_or_default();
let tokens: Vec<&str> = vary.split(',').map(str::trim).collect();
assert!(
tokens.contains(&"Origin"),
"Vary lost pre-existing Origin token: {vary}"
);
assert!(
tokens
.iter()
.any(|t| t.eq_ignore_ascii_case("Accept-Encoding")),
"Vary missing Accept-Encoding token: {vary}"
);
}
#[tokio::test]
async fn test_compress_offloads_large_body_and_round_trips() {
let mw = Compress::new(CompressionLevel::Best);
let mut req = HttpRequest::new("GET", "/");
req.headers.insert("accept-encoding", "gzip");
let original: Vec<u8> = (0..(GZIP_OFFLOAD_THRESHOLD * 4))
.map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
.collect();
assert!(original.len() > GZIP_OFFLOAD_THRESHOLD);
let resp = mw.call(req, body_next(original.clone())).await.unwrap();
assert_eq!(
resp.headers.get("Content-Encoding").map(String::as_str),
Some("gzip")
);
assert_eq!(gunzip(resp.body_ref()), original);
}
#[tokio::test]
async fn test_compress_handles_bytes_backed_body_above_offload_threshold() {
let mw = Compress::new(CompressionLevel::Best);
let mut req = HttpRequest::new("GET", "/");
req.headers.insert("accept-encoding", "gzip");
let original: Vec<u8> = (0..(GZIP_OFFLOAD_THRESHOLD * 4))
.map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
.collect();
assert!(original.len() > GZIP_OFFLOAD_THRESHOLD);
let bytes_body = bytes::Bytes::from(original.clone());
let next: Next = Box::new(move |_req: HttpRequest| {
Box::pin(async move { Ok(HttpResponse::ok().with_bytes_body(bytes_body)) })
});
let resp = mw.call(req, next).await.unwrap();
assert_eq!(
resp.headers.get("Content-Encoding").map(String::as_str),
Some("gzip")
);
assert!(
!resp.body_ref().is_empty(),
"compressed body must not be empty"
);
assert_eq!(gunzip(resp.body_ref()), original);
}
#[tokio::test]
async fn test_gzip_encode_offloaded_round_trips_inline() {
let original: Vec<u8> = (0..4096)
.map(|i: usize| (i.wrapping_mul(2654435761) >> 13) as u8)
.collect();
assert!(original.len() < GZIP_OFFLOAD_THRESHOLD);
let compressed = gzip_encode_offloaded(original.clone(), CompressionLevel::Best)
.await
.expect("inline gzip encode should succeed");
assert!(compressed.len() < original.len());
assert_eq!(gunzip(&compressed), original);
}
#[tokio::test]
async fn test_gzip_encode_offloaded_round_trips_offload_path() {
let original: Vec<u8> = (0..(GZIP_OFFLOAD_THRESHOLD * 4))
.map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
.collect();
assert!(original.len() > GZIP_OFFLOAD_THRESHOLD);
let compressed = gzip_encode_offloaded(original.clone(), CompressionLevel::Best)
.await
.expect("offloaded gzip encode should succeed");
assert!(compressed.len() < original.len());
assert_eq!(gunzip(&compressed), original);
}
async fn body_len_handler(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok().with_body(req.body.len().to_string().into_bytes()))
}
async fn spawn_test_server(app: BuiltApp) -> std::net::SocketAddr {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("resolve local_addr");
let app = Arc::new(app);
tokio::spawn(async move {
let _ = serve(listener, app).await;
});
addr
}
async fn send_raw_request(addr: std::net::SocketAddr, request: &[u8]) -> String {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut stream = tokio::net::TcpStream::connect(addr)
.await
.expect("connect to test server");
stream.write_all(request).await.expect("write request");
let mut buf = Vec::new();
let _ = tokio::time::timeout(
std::time::Duration::from_secs(5),
stream.read_to_end(&mut buf),
)
.await;
String::from_utf8_lossy(&buf).into_owned()
}
#[tokio::test]
async fn test_serve_rejects_oversized_content_length_before_buffering_body() {
let app = App::new()
.with_max_body_size(1024)
.route("/", post(body_len_handler))
.build();
let addr = spawn_test_server(app).await;
let request = b"POST / HTTP/1.1\r\n\
Host: test\r\n\
Content-Length: 10485760\r\n\
Connection: close\r\n\
\r\n";
let response = send_raw_request(addr, request).await;
let status_line = response.lines().next().unwrap_or_default();
assert!(
status_line.contains("413"),
"expected 413 status line, got: {status_line:?} (full response: {response:?})"
);
assert!(
response.contains("Payload Too Large"),
"expected canonical 413 body, got: {response:?}"
);
}
#[tokio::test]
async fn test_serve_rejects_oversized_chunked_body_via_limited_stream() {
let app = App::new()
.with_max_body_size(10)
.route("/", post(body_len_handler))
.build();
let addr = spawn_test_server(app).await;
let request = b"POST / HTTP/1.1\r\n\
Host: test\r\n\
Transfer-Encoding: chunked\r\n\
Connection: close\r\n\
\r\n\
14\r\n\
01234567890123456789\r\n\
0\r\n\
\r\n";
let response = send_raw_request(addr, request).await;
let status_line = response.lines().next().unwrap_or_default();
assert!(
status_line.contains("413"),
"expected 413 status line, got: {status_line:?} (full response: {response:?})"
);
}
#[tokio::test]
async fn test_serve_rejects_malformed_chunked_body_with_400() {
let app = App::new()
.with_max_body_size(1024)
.route("/", post(body_len_handler))
.build();
let addr = spawn_test_server(app).await;
let request = b"POST / HTTP/1.1\r\n\
Host: test\r\n\
Transfer-Encoding: chunked\r\n\
Connection: close\r\n\
\r\n\
ZZ\r\n\
data\r\n\
0\r\n\
\r\n";
let response = send_raw_request(addr, request).await;
let status_line = response.lines().next().unwrap_or_default();
assert!(
status_line.contains("400"),
"expected 400 status line, got: {status_line:?} (full response: {response:?})"
);
}
#[tokio::test]
async fn test_serve_accepts_body_within_limit() {
let app = App::new()
.with_max_body_size(1024)
.route("/", post(body_len_handler))
.build();
let addr = spawn_test_server(app).await;
let body = b"hello world";
let request = format!(
"POST / HTTP/1.1\r\nHost: test\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
String::from_utf8_lossy(body)
);
let response = send_raw_request(addr, request.as_bytes()).await;
let status_line = response.lines().next().unwrap_or_default();
assert!(
status_line.contains("200"),
"expected 200 status line, got: {status_line:?} (full response: {response:?})"
);
assert!(
response.ends_with(&body.len().to_string()),
"expected echoed body length {}, got: {response:?}",
body.len()
);
}
}