use std::future::Future;
use std::net::SocketAddr;
use std::io::Write as _;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use hyper::header::{HeaderName, HeaderValue};
use hyper::body::Body as HttpBody;
use hyper::body::Bytes;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::{Method, Request, Response, StatusCode};
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use hyper_util::rt::TokioTimer;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Empty, Full};
use tokio::net::{TcpListener, TcpStream};
use crate::body::{MaxBodySize, DEFAULT_MAX_BODY_SIZE};
use crate::cors::CorsConfig;
use crate::error::ServeError;
use crate::handler::{Handler, Middleware, OnUpgrade, ResponseBody};
use crate::router::{QueryParams, Router, PathSegments};
use crate::state::State;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PeerAddr(pub SocketAddr);
const MAX_PATH_LEN: usize = 8_192;
const MAX_QUERY_LEN: usize = 4_096;
const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_MAX_CONNECTIONS: usize = 1024;
pub const DEFAULT_MAX_HEADER_BYTES: usize = 64 * 1024;
const CONNECTION_OWNED_HEADERS: [HeaderName; 3] = [
hyper::header::CONTENT_LENGTH,
hyper::header::CONNECTION,
hyper::header::TRANSFER_ENCODING,
];
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const UPGRADE_HANDOFF_TIMEOUT: Duration = Duration::from_secs(10);
const ACCEPT_BACKOFF_INITIAL: Duration = Duration::from_millis(10);
const ACCEPT_BACKOFF_MAX: Duration = Duration::from_secs(1);
#[cfg(test)]
thread_local! {
static ERROR_LOG: std::cell::RefCell<Vec<(u16, String)>> = const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(test)]
fn capture_error(code: u16, message: String) {
ERROR_LOG.with(|log| log.borrow_mut().push((code, message)));
}
#[cfg(test)]
fn take_error_log() -> Vec<(u16, String)> {
ERROR_LOG.with(|log| log.borrow_mut().drain(..).collect())
}
trait TcpAccept {
async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)>;
}
impl TcpAccept for TcpListener {
async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
TcpListener::accept(self).await
}
}
struct Backoff {
delay: Duration,
}
impl Backoff {
fn new() -> Self {
Backoff { delay: ACCEPT_BACKOFF_INITIAL }
}
fn next_delay(&mut self) -> Duration {
let delay = self.delay;
self.delay = (self.delay * 2).min(ACCEPT_BACKOFF_MAX);
delay
}
fn reset(&mut self) {
self.delay = ACCEPT_BACKOFF_INITIAL;
}
}
pub type ErrorHandler =
Arc<dyn Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync>;
pub(crate) type LogSink = Arc<Mutex<Box<dyn std::io::Write + Send>>>;
pub struct App<S> {
state: Arc<S>,
log: Option<LogSink>,
extra_headers: Arc<Vec<(HeaderName, HeaderValue)>>,
router: Arc<Router<S>>,
max_body_size: usize,
pub(crate) upgrades_enabled: bool,
pub(crate) header_read_timeout: Duration,
pub(crate) max_header_bytes: usize,
pub(crate) max_connections: usize,
pub(crate) connect_timeout: Duration,
error_handler: ErrorHandler,
cors_config: Option<CorsConfig>,
fallback: Option<Handler<S>>,
}
pub(crate) fn report_if_panicked(sink: &Option<LogSink>, joined: Result<(), tokio::task::JoinError>) {
let Err(e) = joined else {
return;
};
if e.is_panic() {
log_line(sink, format_args!("connection task panicked: {e}"));
}
}
pub(crate) fn log_line(sink: &Option<LogSink>, line: std::fmt::Arguments<'_>) {
let Some(sink) = sink else {
return;
};
if let Ok(mut out) = sink.lock() {
let _ = writeln!(out, "{line}");
let _ = out.flush();
}
}
fn parse_query(query: &str) -> QueryParams {
let mut map = std::collections::HashMap::new();
for pair in query.split('&').filter(|s| !s.is_empty()) {
if let Some((key, value)) = pair.split_once('=') {
let key = decode_query_component(key);
let value = decode_query_component(value);
map.insert(key, value);
} else {
let pair = decode_query_component(pair);
map.insert(pair, String::new());
}
}
QueryParams(map)
}
fn decode_query_component(s: &str) -> String {
let with_spaces = s.replace('+', " ");
percent_encoding::percent_decode_str(&with_spaces)
.decode_utf8_lossy()
.into_owned()
}
fn error_response(status: StatusCode, message: &str) -> Response<ResponseBody> {
#[cfg(test)]
capture_error(status.as_u16(), message.to_string());
let client_message = if status.is_server_error() {
"internal server error"
} else {
message
};
let body = serde_json::json!({ "message": client_message });
let json = serde_json::to_string(&body)
.unwrap_or_else(|_| r#"{"message":"internal server error"}"#.to_string());
let mut resp = Response::new(BoxBody::new(
Full::new(Bytes::from(json)).map_err(|never: std::convert::Infallible| match never {}),
));
*resp.status_mut() = status;
resp.headers_mut().insert(
hyper::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
resp
}
fn default_error_handler() -> ErrorHandler {
Arc::new(error_response)
}
fn ephemeral_bind_addr() -> SocketAddr {
(std::net::Ipv4Addr::LOCALHOST, 0).into()
}
impl<S: Send + Sync + 'static> App<S> {
pub fn new(state: S) -> Self {
App {
state: Arc::new(state),
router: Arc::new(Router::new()),
max_body_size: DEFAULT_MAX_BODY_SIZE,
header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
max_header_bytes: DEFAULT_MAX_HEADER_BYTES,
upgrades_enabled: false,
log: None,
extra_headers: Arc::new(Vec::new()),
max_connections: DEFAULT_MAX_CONNECTIONS,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
error_handler: default_error_handler(),
cors_config: None,
fallback: None,
}
}
pub fn state_arc(&self) -> Arc<S> {
Arc::clone(&self.state)
}
pub async fn route(&self, req: Request<Incoming>) -> Response<ResponseBody> {
self.route_with(req, None).await
}
pub async fn route_with_peer(&self, req: Request<Incoming>, peer: SocketAddr) -> Response<ResponseBody> {
self.route_with(req, Some(peer)).await
}
async fn route_with(&self, req: Request<Incoming>, peer: Option<SocketAddr>) -> Response<ResponseBody> {
let method = req.method().clone();
let req_origin = req
.headers()
.get(hyper::header::ORIGIN)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let mut resp = self.route_inner(req, peer, req_origin.as_deref()).await;
self.finalize(&mut resp, req_origin.as_deref());
if method == Method::HEAD {
let (mut parts, body) = resp.into_parts();
if !parts.headers.contains_key(hyper::header::CONTENT_LENGTH) {
if let Some(len) = HttpBody::size_hint(&body).exact() {
parts.headers.insert(hyper::header::CONTENT_LENGTH, len.into());
}
}
Response::from_parts(parts, BoxBody::new(Empty::new().map_err(|never: std::convert::Infallible| match never {})))
} else {
resp
}
}
async fn invoke(
&self,
handler: &Handler<S>,
req: Request<Incoming>,
state: State<S>,
path: &str,
) -> Response<ResponseBody> {
match handler(req, state).await {
Ok(resp) => resp,
Err(e) => {
let status =
StatusCode::from_u16(e.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
if status.is_server_error() {
log_line(&self.log, format_args!("{status} {}: {}", path, e.message));
}
(self.error_handler)(status, &e.message)
}
}
}
fn finalize(&self, resp: &mut Response<ResponseBody>, req_origin: Option<&str>) {
if let Some(cfg) = &self.cors_config {
cfg.apply_to_response(resp, req_origin);
}
let headers = resp.headers_mut();
headers
.entry(hyper::header::X_CONTENT_TYPE_OPTIONS)
.or_insert(HeaderValue::from_static("nosniff"));
for (name, value) in self.extra_headers.iter() {
headers.entry(name).or_insert(value.clone());
}
}
async fn route_inner(
&self,
req: Request<Incoming>,
peer: Option<SocketAddr>,
req_origin: Option<&str>,
) -> Response<ResponseBody> {
if req.version() == hyper::Version::HTTP_11
&& req.uri().authority().is_none()
&& !req.headers().contains_key(hyper::header::HOST)
{
return (self.error_handler)(StatusCode::BAD_REQUEST, "missing host header");
}
if req.uri().path().len() > MAX_PATH_LEN {
return (self.error_handler)(StatusCode::BAD_REQUEST, "path too long");
}
if req.uri().query().map(|q| q.len()).unwrap_or(0) > MAX_QUERY_LEN {
return (self.error_handler)(StatusCode::BAD_REQUEST, "query string too long");
}
let method = req.method().clone();
let path = req.uri().path().to_string();
let state = State::from_arc(Arc::clone(&self.state));
let query = req.uri().query().unwrap_or("");
let query_params = if query.is_empty() {
QueryParams::default()
} else {
parse_query(query)
};
let segments = crate::router::split_path(&path);
if method == Method::OPTIONS && req_origin.is_some() {
if let Some(cfg) = &self.cors_config {
if self.router.segments_exist(&segments) {
let requested_headers = req
.headers()
.get("access-control-request-headers")
.and_then(|v| v.to_str().ok());
let allowed = self.allowed_methods_with_head(&segments);
return cfg.preflight_response(req_origin, requested_headers, &allowed);
}
}
}
let method_to_match = if method == Method::HEAD {
Method::GET
} else {
method.clone()
};
match self.router.match_segments(&method_to_match, &segments) {
Some((handler, params)) => {
let mut req = req;
if !query_params.0.is_empty() {
req.extensions_mut().insert(query_params);
}
if !params.0.is_empty() {
req.extensions_mut().insert(params);
}
if self.max_body_size != DEFAULT_MAX_BODY_SIZE {
req.extensions_mut().insert(MaxBodySize(self.max_body_size));
}
if let Some(peer) = peer {
req.extensions_mut().insert(PeerAddr(peer));
}
self.invoke(handler, req, state, &path).await
}
None => {
let allowed = self.allowed_methods_with_head(&segments);
if !allowed.is_empty() {
let mut method_strs: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
method_strs.sort();
method_strs.dedup();
let allow_header = method_strs.join(", ");
let mut resp = (self.error_handler)(StatusCode::METHOD_NOT_ALLOWED, "method not allowed");
if let Ok(val) = allow_header.parse() {
resp.headers_mut().insert("allow", val);
}
resp
} else if let Some(fallback) = self.fallback.clone() {
let state = State::from_arc(Arc::clone(&self.state));
let mut req = req;
if self.max_body_size != DEFAULT_MAX_BODY_SIZE {
req.extensions_mut().insert(MaxBodySize(self.max_body_size));
}
if let Some(peer) = peer {
req.extensions_mut().insert(PeerAddr(peer));
}
req.extensions_mut().insert(PathSegments(segments));
self.invoke(&fallback, req, state, &path).await
} else {
(self.error_handler)(StatusCode::NOT_FOUND, "not found")
}
}
}
}
fn allowed_methods_with_head(&self, segments: &[String]) -> Vec<Method> {
let mut allowed = self.router.allowed_methods_for(segments);
if allowed.contains(&Method::GET) {
allowed.push(Method::HEAD);
}
allowed
}
pub async fn bind_ephemeral(self) -> Result<u16, ServeError> {
let listener = TcpListener::bind(ephemeral_bind_addr())
.await
.map_err(|e| ServeError::new(500, format!("failed to bind to ephemeral port: {e}")))?;
let port = listener
.local_addr()
.map_err(|e| ServeError::new(500, format!("failed to get assigned port: {e}")))?
.port();
let app = Arc::new(self);
tokio::spawn(async move {
serve_loop(listener, app, std::future::pending(), plain_connect).await;
});
Ok(port)
}
pub async fn run<F>(self, listener: TcpListener, shutdown: F) -> Result<(), ServeError>
where
F: Future<Output = ()> + Send + 'static,
{
self.run_with_transport(listener, shutdown, plain_connect).await
}
pub async fn run_with_transport<F, C, Fut, IO>(
self,
listener: TcpListener,
shutdown: F,
connect: C,
) -> Result<(), ServeError>
where
F: Future<Output = ()> + Send + 'static,
C: Fn(TcpStream) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<IO>> + Send + 'static,
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let app = Arc::new(self);
serve_loop(listener, app, shutdown, connect).await;
Ok(())
}
pub async fn bind(self, addr: SocketAddr) -> Result<(), ServeError> {
let shutdown = shutdown_signal()?;
let listener = TcpListener::bind(addr)
.await
.map_err(|e| ServeError::new(500, format!("failed to bind to {addr}: {e}")))?;
self.run(listener, shutdown).await
}
}
impl App<()> {
pub fn stateless() -> Self {
App::new(())
}
}
async fn serve_connection<S, IO>(io: IO, app: Arc<App<S>>, header_read_timeout: Duration, peer: SocketAddr)
where
S: Send + Sync + 'static,
IO: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
{
let app_for_conn = app.clone();
let log_for_conn = app.log.clone();
let pending_upgrade: Arc<Mutex<Option<(hyper::upgrade::OnUpgrade, OnUpgrade)>>> =
Arc::new(Mutex::new(None));
let pending_for_service = pending_upgrade.clone();
let svc = service_fn(move |mut req: Request<Incoming>| {
let app = app.clone();
let pending = pending_for_service.clone();
let upgrade = hyper::upgrade::on(&mut req);
async move {
let observed = app.log.as_ref().map(|_| {
(
std::time::Instant::now(),
req.method().clone(),
req.uri().path().to_string(),
)
});
let resp = app.route_with_peer(req, peer).await;
if let Some((started, method, path)) = observed {
log_line(
&app.log,
format_args!(
"{method} {path} {} {:.3}ms",
resp.status().as_u16(),
started.elapsed().as_secs_f64() * 1000.0
),
);
}
let mut resp = resp;
if let Some(callback) = resp.extensions_mut().remove::<OnUpgrade>() {
if app.upgrades_enabled {
*pending.lock().unwrap() = Some((upgrade, callback));
} else {
log_line(
&app.log,
format_args!(
"handler returned an upgrade for {} but the app was not built \
with with_upgrades(); the connection will not be upgraded",
resp.status().as_u16()
),
);
}
}
Ok::<_, hyper::Error>(resp)
}
});
let mut builder = http1::Builder::new();
builder.timer(TokioTimer::new());
builder.header_read_timeout(header_read_timeout);
builder.max_buf_size(app_for_conn.max_header_bytes);
if app_for_conn.upgrades_enabled {
let _ = builder.serve_connection(io, svc).with_upgrades().await;
} else {
let _ = builder.serve_connection(io, svc).await;
}
let taken = pending_upgrade.lock().unwrap().take();
if let Some((upgrade, callback)) = taken {
match tokio::time::timeout(UPGRADE_HANDOFF_TIMEOUT, upgrade).await {
Ok(Ok(upgraded)) => callback.run(TokioIo::new(upgraded)).await,
Ok(Err(e)) => log_line(&log_for_conn, format_args!("upgrade failed: {e}")),
Err(_) => log_line(
&log_for_conn,
format_args!(
"upgrade did not complete within {UPGRADE_HANDOFF_TIMEOUT:?}; \
releasing the connection"
),
),
}
}
}
async fn plain_connect(stream: TcpStream) -> Option<TcpStream> {
Some(stream)
}
async fn accept_and_permit<L: TcpAccept>(
listener: &L,
backoff: &mut Backoff,
semaphore: &Arc<tokio::sync::Semaphore>,
) -> Option<(TcpStream, SocketAddr, tokio::sync::OwnedSemaphorePermit)> {
let permit = semaphore.clone().acquire_owned().await.ok()?;
loop {
match listener.accept().await {
Ok((stream, peer)) => {
backoff.reset();
return Some((stream, peer, permit));
}
Err(_) => tokio::time::sleep(backoff.next_delay()).await,
}
}
}
fn signal_install_error(signal: &str, cause: std::io::Error) -> ServeError {
ServeError::new(500, format!("failed to install {signal} handler: {cause}"))
}
pub fn shutdown_signal() -> Result<impl Future<Output = ()> + Send, ServeError> {
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
.map_err(|e| signal_install_error("SIGINT", e))?;
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.map_err(|e| signal_install_error("SIGTERM", e))?;
Ok(async move {
tokio::select! {
_ = sigint.recv() => {}
_ = sigterm.recv() => {}
}
})
}
async fn serve_loop<S, F, C, Fut, IO>(listener: TcpListener, app: Arc<App<S>>, shutdown: F, connect: C)
where
S: Send + Sync + 'static,
F: Future<Output = ()> + Send + 'static,
C: Fn(TcpStream) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Option<IO>> + Send + 'static,
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let header_read_timeout = app.header_read_timeout;
let connect_timeout = app.connect_timeout;
let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
let connect = Arc::new(connect);
let mut backoff = Backoff::new();
let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
let mut shutdown_pin = std::pin::pin!(shutdown);
let mut shutting_down = false;
loop {
if !shutting_down {
tokio::select! {
accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
match accepted {
Some((stream, peer, permit)) => {
let app = app.clone();
let connect = connect.clone();
join_set.spawn(async move {
let _permit = permit;
let negotiated =
tokio::time::timeout(connect_timeout, connect(stream)).await;
if let Ok(Some(io)) = negotiated {
serve_connection(TokioIo::new(io), app, header_read_timeout, peer).await;
}
});
}
None => shutting_down = true,
}
}
joined = join_set.join_next(), if !join_set.is_empty() => {
if let Some(joined) = joined {
report_if_panicked(&app.log, joined);
}
}
_ = shutdown_pin.as_mut() => {
shutting_down = true;
}
}
continue;
}
let drained = tokio::time::timeout(DEFAULT_SHUTDOWN_DRAIN_TIMEOUT, async {
while let Some(joined) = join_set.join_next().await {
report_if_panicked(&app.log, joined);
}
})
.await;
if drained.is_err() {
join_set.shutdown().await;
}
break;
}
}
#[must_use = "RouteBuilder does nothing until .seal() is called"]
pub struct RouteBuilder<S> {
state: Arc<S>,
router: Router<S>,
max_body_size: usize,
header_read_timeout: Duration,
log: Option<LogSink>,
extra_headers: Arc<Vec<(HeaderName, HeaderValue)>>,
max_connections: usize,
connect_timeout: Duration,
error_handler: ErrorHandler,
cors_config: Option<CorsConfig>,
middlewares: Vec<Middleware<S>>,
fallback: Option<Handler<S>>,
max_header_bytes: usize,
upgrades_enabled: bool,
}
impl<S: Send + Sync + 'static> RouteBuilder<S> {
pub fn new(state: S) -> Self {
RouteBuilder {
state: Arc::new(state),
router: Router::new(),
max_body_size: DEFAULT_MAX_BODY_SIZE,
header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
upgrades_enabled: false,
log: None,
extra_headers: Arc::new(Vec::new()),
max_connections: DEFAULT_MAX_CONNECTIONS,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
error_handler: default_error_handler(),
cors_config: None,
middlewares: Vec::new(),
fallback: None,
max_header_bytes: DEFAULT_MAX_HEADER_BYTES,
}
}
pub fn wrap(mut self, middleware: Middleware<S>) -> Self {
self.middlewares.push(middleware);
self
}
fn apply_middlewares(&self, handler: Handler<S>) -> Handler<S> {
self.middlewares.iter().rev().fold(handler, |acc, mw| mw(acc))
}
pub fn with_fallback(mut self, handler: Handler<S>) -> Self {
self.fallback = Some(self.apply_middlewares(handler));
self
}
pub fn with_max_body_size(mut self, max: usize) -> Self {
self.max_body_size = max;
self
}
pub fn with_header_read_timeout(mut self, d: Duration) -> Self {
self.header_read_timeout = d;
self
}
pub fn with_response_header(mut self, name: &str, value: &str) -> Result<Self, ServeError> {
let name = HeaderName::from_bytes(name.as_bytes())
.map_err(|_| ServeError::new(500, format!("invalid header name: {name}")))?;
let value = HeaderValue::from_str(value)
.map_err(|_| ServeError::new(500, format!("invalid value for header {name}")))?;
if CONNECTION_OWNED_HEADERS.contains(&name) {
return Err(ServeError::new(
500,
format!("{name} is owned by the connection layer and cannot be set as a fixed header"),
));
}
Arc::make_mut(&mut self.extra_headers).push((name, value));
Ok(self)
}
pub fn with_request_logging(self) -> Self {
self.with_request_logging_to(Box::new(std::io::stderr()))
}
pub fn with_request_logging_to(mut self, writer: Box<dyn std::io::Write + Send>) -> Self {
self.log = Some(Arc::new(Mutex::new(writer)));
self
}
pub fn with_connect_timeout(mut self, d: Duration) -> Self {
self.connect_timeout = d;
self
}
pub fn with_max_header_bytes(mut self, bytes: usize) -> Self {
self.max_header_bytes = bytes;
self
}
pub fn with_upgrades(mut self) -> Self {
self.upgrades_enabled = true;
self
}
pub fn with_max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
pub fn with_error_handler(
mut self,
f: impl Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync + 'static,
) -> Self {
self.error_handler = Arc::new(f);
self
}
pub fn with_cors(mut self, config: CorsConfig) -> Self {
self.cors_config = Some(config);
self
}
pub fn get(mut self, path: &str, handler: Handler<S>) -> Self {
let handler = self.apply_middlewares(handler);
self.router.insert(Method::GET, path, handler);
self
}
pub fn post(mut self, path: &str, handler: Handler<S>) -> Self {
let handler = self.apply_middlewares(handler);
self.router.insert(Method::POST, path, handler);
self
}
pub fn put(mut self, path: &str, handler: Handler<S>) -> Self {
let handler = self.apply_middlewares(handler);
self.router.insert(Method::PUT, path, handler);
self
}
pub fn delete(mut self, path: &str, handler: Handler<S>) -> Self {
let handler = self.apply_middlewares(handler);
self.router.insert(Method::DELETE, path, handler);
self
}
pub fn patch(mut self, path: &str, handler: Handler<S>) -> Self {
let handler = self.apply_middlewares(handler);
self.router.insert(Method::PATCH, path, handler);
self
}
pub fn seal(self) -> App<S> {
App {
state: self.state,
router: Arc::new(self.router),
max_body_size: self.max_body_size,
header_read_timeout: self.header_read_timeout,
upgrades_enabled: self.upgrades_enabled,
log: self.log,
extra_headers: self.extra_headers,
max_connections: self.max_connections,
connect_timeout: self.connect_timeout,
error_handler: self.error_handler,
cors_config: self.cors_config,
fallback: self.fallback,
max_header_bytes: self.max_header_bytes,
}
}
}
impl RouteBuilder<()> {
pub fn stateless() -> Self {
RouteBuilder::new(())
}
}
#[cfg(test)]
#[path = "../tests/unit/app.rs"]
mod tests;