use crate::Limits;
use crate::conn::ConnConfig;
use crate::service::{H1Service, Transport};
use crate::tls::{H2Fallback, Preface, UpgradeConsumer, is_h2c_preface};
use crate::write::DateCache;
use bytes::{Bytes, BytesMut};
use std::cell::RefCell;
use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
use tokio::net::TcpListener;
use tokio::sync::watch;
const ACCEPT_BACKOFF: Duration = Duration::from_millis(10);
const WORKER_START_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(any(target_os = "linux", target_os = "android"))]
const EPROTO: Option<i32> = Some(71);
#[cfg(any(target_os = "macos", target_os = "ios"))]
const EPROTO: Option<i32> = Some(100);
#[cfg(target_os = "freebsd")]
const EPROTO: Option<i32> = Some(92);
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios",
target_os = "freebsd"
)))]
const EPROTO: Option<i32> = None;
fn accept_backoff_warranted(e: &io::Error) -> bool {
match e.kind() {
io::ErrorKind::ConnectionAborted
| io::ErrorKind::ConnectionReset
| io::ErrorKind::Interrupted
| io::ErrorKind::TimedOut => false,
_ => !matches!((EPROTO, e.raw_os_error()), (Some(p), Some(r)) if p == r),
}
}
#[derive(Clone, Debug)]
pub struct TcpConfig {
pub nodelay: bool,
pub backlog: i32,
pub reuse_port: bool,
}
impl Default for TcpConfig {
fn default() -> Self {
Self {
nodelay: true,
backlog: 1024,
reuse_port: true,
}
}
}
#[derive(Clone, Debug)]
pub struct Config {
pub addr: SocketAddr,
pub workers: usize,
pub limits: Limits,
pub tcp: TcpConfig,
pub tick: Duration,
pub pin_cores: bool,
pub server_name: Option<Bytes>,
pub shutdown_grace: Duration,
pub detect_h2c: bool,
#[cfg(feature = "tls")]
pub tls: Option<std::sync::Arc<rustls::ServerConfig>>,
}
impl Config {
pub fn new(addr: SocketAddr) -> Self {
Self {
addr,
workers: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
limits: Limits::default(),
tcp: TcpConfig::default(),
tick: Duration::from_millis(100),
pin_cores: true,
server_name: None,
shutdown_grace: Duration::from_secs(10),
detect_h2c: false,
#[cfg(feature = "tls")]
tls: None,
}
}
pub fn detect_h2c(mut self, on: bool) -> Self {
self.detect_h2c = on;
self
}
#[cfg(feature = "tls")]
pub fn with_tls(mut self, tls: std::sync::Arc<rustls::ServerConfig>) -> Self {
self.tls = Some(tls);
self
}
pub fn workers(mut self, n: usize) -> Self {
self.workers = n.max(1);
self
}
pub fn limits(mut self, mut limits: Limits) -> Self {
limits.clamp_max_headers();
self.limits = limits;
self
}
pub fn server_name(mut self, name: Bytes) -> Self {
self.server_name = Some(name);
self
}
pub fn pin_cores(mut self, on: bool) -> Self {
self.pin_cores = on;
self
}
}
#[derive(Clone, Debug)]
pub struct ServerHandle {
tx: watch::Sender<bool>,
}
impl ServerHandle {
pub fn shutdown(&self) {
let _ = self.tx.send_replace(true);
}
pub fn is_shutting_down(&self) -> bool {
*self.tx.borrow()
}
}
enum Listeners {
PerWorker(Vec<std::net::TcpListener>),
Shared(Arc<std::net::TcpListener>),
}
pub struct Server {
cfg: Config,
listeners: Listeners,
local_addr: SocketAddr,
tx: watch::Sender<bool>,
}
impl Server {
pub fn bind(cfg: Config) -> io::Result<Self> {
let (listeners, local_addr) = if cfg.tcp.reuse_port && reuse_port_supported() {
let mut v = Vec::with_capacity(cfg.workers);
let mut addr = cfg.addr;
for i in 0..cfg.workers {
let l = bind_one(addr, &cfg.tcp, true)?;
if i == 0 {
addr = l.local_addr()?;
}
v.push(l);
}
(Listeners::PerWorker(v), addr)
} else {
let l = bind_one(cfg.addr, &cfg.tcp, false)?;
let addr = l.local_addr()?;
(Listeners::Shared(Arc::new(l)), addr)
};
let (tx, _) = watch::channel(false);
Ok(Self {
cfg,
listeners,
local_addr,
tx,
})
}
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub fn handle(&self) -> ServerHandle {
ServerHandle {
tx: self.tx.clone(),
}
}
pub fn serve<F, S>(self, make: F) -> io::Result<()>
where
F: Fn() -> S + Send + Clone + 'static,
S: H1Service + 'static,
{
self.serve_with_fallback(make, || CloseH2)
}
pub fn serve_with_fallback<F, S, G, H>(self, make: F, make_fallback: G) -> io::Result<()>
where
F: Fn() -> S + Send + Clone + 'static,
S: H1Service + 'static,
G: Fn() -> H + Send + Clone + 'static,
H: H2Fallback + 'static,
{
self.serve_with(make, make_fallback, || CloseUpgrade)
}
pub fn serve_with<F, S, G, H, U, C>(
self,
make: F,
make_fallback: G,
make_upgrade: U,
) -> io::Result<()>
where
F: Fn() -> S + Send + Clone + 'static,
S: H1Service + 'static,
G: Fn() -> H + Send + Clone + 'static,
H: H2Fallback + 'static,
U: Fn() -> C + Send + Clone + 'static,
C: UpgradeConsumer + 'static,
{
let Server {
cfg, listeners, tx, ..
} = self;
let core_ids = if cfg.pin_cores {
core_affinity::get_core_ids().unwrap_or_default()
} else {
Vec::new()
};
let mut per_worker: Vec<Option<std::net::TcpListener>> = match listeners {
Listeners::PerWorker(v) => v.into_iter().map(Some).collect(),
Listeners::Shared(shared) => (0..cfg.workers)
.map(|_| match shared.try_clone() {
Ok(l) => Some(l),
Err(e) => {
tracing::warn!(error = %e, "failed to clone the shared listener for a worker; that worker will not start");
None
}
})
.collect(),
};
let base_rx = tx.subscribe();
let started = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::with_capacity(cfg.workers);
for (worker, slot) in per_worker.iter_mut().enumerate() {
let cfg = cfg.clone();
let make = make.clone();
let make_fallback = make_fallback.clone();
let make_upgrade = make_upgrade.clone();
let rx = base_rx.clone();
let core = core_ids.get(worker).copied();
let started = started.clone();
let Some(std_listener) = slot.take() else {
continue;
};
let spawned = std::thread::Builder::new()
.name(format!("h1-{worker}"))
.spawn(move || {
if let Some(core) = core {
core_affinity::set_for_current(core);
}
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
tracing::error!(
error = %e,
worker,
"worker could not build its runtime and will not serve"
);
return;
}
};
rt.block_on(worker_loop(
std_listener,
make,
make_fallback,
make_upgrade,
cfg,
rx,
Startup {
worker,
started: &started,
},
));
});
match spawned {
Ok(h) => handles.push(h),
Err(e) => {
tracing::error!(error = %e, worker, "failed to spawn a worker; stopping the ones already started");
let _ = tx.send_replace(true);
join_all(handles);
return Err(e);
}
}
}
let expected = handles.len();
let deadline = std::time::Instant::now() + WORKER_START_TIMEOUT;
while started.load(Ordering::Acquire) < expected {
if std::time::Instant::now() >= deadline {
let live = started.load(Ordering::Acquire);
tracing::error!(
started = live,
expected,
"workers failed to reach their accept loop; stopping the ones that did"
);
let _ = tx.send_replace(true);
join_all(handles);
return Err(io::Error::other(format!(
"only {live} of {expected} workers reached their accept loop"
)));
}
std::thread::sleep(Duration::from_millis(1));
}
let panicked = join_all(handles);
if panicked > 0 {
return Err(io::Error::other(format!(
"{panicked} worker thread(s) panicked"
)));
}
Ok(())
}
}
fn join_all(handles: Vec<std::thread::JoinHandle<()>>) -> usize {
let mut panicked = 0;
for h in handles {
if h.join().is_err() {
panicked += 1;
}
}
if panicked > 0 {
tracing::error!(panicked, "worker thread(s) panicked");
}
panicked
}
struct Startup<'a> {
worker: usize,
started: &'a AtomicUsize,
}
async fn worker_loop<F, S, G, H, U, C>(
std_listener: std::net::TcpListener,
make: F,
make_fallback: G,
make_upgrade: U,
cfg: Config,
mut rx: watch::Receiver<bool>,
startup: Startup<'_>,
) where
F: Fn() -> S,
S: H1Service + 'static,
G: Fn() -> H,
H: H2Fallback + 'static,
U: Fn() -> C,
C: UpgradeConsumer + 'static,
{
let Startup { worker, started } = startup;
std_listener.set_nonblocking(true).ok();
let listener = match TcpListener::from_std(std_listener) {
Ok(l) => l,
Err(e) => {
tracing::error!(
error = %e,
worker,
"worker could not register its listener and will not serve"
);
return;
}
};
started.fetch_add(1, Ordering::Release);
let mut limits = cfg.limits.clone();
let cfg = Rc::new(cfg);
limits.clamp_max_headers();
let conn_cfg = Rc::new(ConnConfig {
limits,
tick: cfg.tick,
server_name: cfg.server_name.clone(),
});
let ctx = Rc::new(WorkerCtx {
service: Rc::new(make()),
fallback: Rc::new(make_fallback()),
upgrades: Rc::new(make_upgrade()),
conn_cfg,
date: Rc::new(RefCell::new(DateCache::new())),
cfg: cfg.clone(),
});
let local = tokio::task::LocalSet::new();
local
.run_until(async {
loop {
if *rx.borrow() {
break;
}
tokio::select! {
changed = rx.changed() => {
if changed.is_err() || *rx.borrow() {
break;
}
}
accepted = listener.accept() => {
let (stream, peer) = match accepted {
Ok(pair) => pair,
Err(e) if !accept_backoff_warranted(&e) => {
tracing::debug!(error = %e, worker, "accept failed on a connection that went away");
continue;
}
Err(e) => {
tracing::warn!(error = %e, worker, "accept failed; pausing before the next attempt");
tokio::select! {
_ = tokio::time::sleep(ACCEPT_BACKOFF) => {}
_ = rx.changed() => {}
}
continue;
}
};
if cfg.tcp.nodelay {
let _ = stream.set_nodelay(true);
}
let ctx = ctx.clone();
tokio::task::spawn_local(async move {
dispatch(stream, ctx, peer).await;
});
}
}
}
})
.await;
let _ = tokio::time::timeout(cfg.shutdown_grace, local).await;
}
struct WorkerCtx<S, H, C> {
service: Rc<S>,
fallback: Rc<H>,
upgrades: Rc<C>,
conn_cfg: Rc<ConnConfig>,
date: Rc<RefCell<DateCache>>,
cfg: Rc<Config>,
}
async fn dispatch<S, H, C>(
stream: tokio::net::TcpStream,
ctx: Rc<WorkerCtx<S, H, C>>,
peer: SocketAddr,
) where
S: H1Service + 'static,
H: H2Fallback + 'static,
C: UpgradeConsumer + 'static,
{
#[cfg(feature = "tls")]
if let Some(tls) = ctx.cfg.tls.clone() {
let acceptor = tokio_rustls::TlsAcceptor::from(tls);
let handshake =
tokio::time::timeout(ctx.cfg.limits.header_timeout, acceptor.accept(stream));
let tls_stream = match handshake.await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
tracing::debug!(%peer, error = %e, "TLS handshake failed");
return;
}
Err(_) => {
tracing::debug!(
%peer,
timeout = ?ctx.cfg.limits.header_timeout,
"TLS handshake timed out"
);
return;
}
};
let is_h2 = crate::tls::negotiated_h2(tls_stream.get_ref().1);
if is_h2 {
ctx.fallback
.handle(Box::new(tls_stream), Bytes::new(), Some(peer))
.await;
} else {
serve_h1(tls_stream, &ctx, Bytes::new(), peer).await;
}
return;
}
if ctx.cfg.detect_h2c {
match peek_preface(stream, &ctx.cfg).await {
Some((stream, buffered, Preface::Http2)) => {
ctx.fallback
.handle(Box::new(stream), buffered, Some(peer))
.await;
}
Some((stream, buffered, _)) => {
serve_h1(stream, &ctx, buffered, peer).await;
}
None => {}
}
return;
}
serve_h1(stream, &ctx, Bytes::new(), peer).await;
}
async fn peek_preface(
mut stream: tokio::net::TcpStream,
cfg: &Config,
) -> Option<(tokio::net::TcpStream, Bytes, Preface)> {
let mut buf = BytesMut::with_capacity(crate::tls::H2C_PREFACE.len());
loop {
match is_h2c_preface(&buf) {
Preface::NeedMore => {}
decided => return Some((stream, buf.freeze(), decided)),
}
let deadline = tokio::time::timeout(cfg.limits.header_timeout, stream.read_buf(&mut buf));
match deadline.await {
Ok(Ok(0)) => {
return Some((stream, buf.freeze(), Preface::Http1));
}
Ok(Ok(_)) => {}
Ok(Err(_)) | Err(_) => return None,
}
}
}
async fn serve_h1<IO, S, H, C>(io: IO, ctx: &WorkerCtx<S, H, C>, buffered: Bytes, peer: SocketAddr)
where
IO: AsyncRead + AsyncWrite + Unpin + 'static,
S: H1Service + 'static,
C: UpgradeConsumer + 'static,
{
let served = crate::backend::serve_connection(
io,
ctx.service.clone(),
ctx.conn_cfg.clone(),
ctx.date.clone(),
buffered,
Some(peer),
)
.await;
match served {
Ok(Some(upgraded)) => ctx.upgrades.handle(upgraded).await,
Ok(None) => {}
Err(e) => match e.kind() {
io::ErrorKind::UnexpectedEof
| io::ErrorKind::ConnectionReset
| io::ErrorKind::BrokenPipe => {}
io::ErrorKind::TimedOut => {
tracing::debug!(%peer, "connection hit a deadline and was closed");
}
_ => {
tracing::debug!(%peer, error = %e, "connection ended with an I/O error");
}
},
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CloseH2;
impl H2Fallback for CloseH2 {
fn handle(
&self,
io: Box<dyn Transport>,
buffered: Bytes,
_peer: Option<SocketAddr>,
) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(async move {
drop(buffered);
drop(io);
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CloseUpgrade;
impl UpgradeConsumer for CloseUpgrade {
fn handle(&self, upgraded: crate::service::Upgraded) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(async move {
drop(upgraded);
})
}
}
fn reuse_port_supported() -> bool {
cfg!(all(
unix,
not(target_os = "solaris"),
not(target_os = "illumos")
))
}
fn bind_one(
addr: SocketAddr,
tcp: &TcpConfig,
reuse_port: bool,
) -> io::Result<std::net::TcpListener> {
let domain = match addr {
SocketAddr::V4(_) => socket2::Domain::IPV4,
SocketAddr::V6(_) => socket2::Domain::IPV6,
};
let socket = socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?;
socket.set_reuse_address(true)?;
#[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))]
if reuse_port {
socket.set_reuse_port(true)?;
}
#[cfg(not(all(unix, not(target_os = "solaris"), not(target_os = "illumos"))))]
let _ = reuse_port;
socket.bind(&addr.into())?;
socket.listen(tcp.backlog)?;
Ok(socket.into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Request, Response};
use std::net::{Ipv4Addr, SocketAddrV4};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn loopback() -> SocketAddr {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
}
fn test_config(workers: usize) -> Config {
let limits = Limits {
idle_timeout: Duration::from_millis(300),
header_timeout: Duration::from_millis(300),
..Default::default()
};
Config::new(loopback())
.workers(workers)
.limits(limits)
.pin_cores(false)
}
async fn hello(_req: Request) -> Response {
Response::text("hi")
}
async fn request(addr: SocketAddr, raw: &[u8]) -> io::Result<String> {
let mut s = tokio::net::TcpStream::connect(addr).await?;
s.write_all(raw).await?;
let mut out = Vec::new();
s.read_to_end(&mut out).await?;
Ok(String::from_utf8_lossy(&out).into_owned())
}
const GET: &[u8] = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n";
fn with_server<Fut, T>(
workers: usize,
body: impl FnOnce(SocketAddr) -> Fut + Send + 'static,
) -> T
where
Fut: std::future::Future<Output = T>,
T: Send + 'static,
{
let server = Server::bind(test_config(workers)).expect("bind");
let addr = server.local_addr();
let handle = server.handle();
let client = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let out = rt.block_on(body(addr));
handle.shutdown();
out
});
server.serve(|| hello).expect("serve");
client.join().expect("client thread")
}
#[test]
fn binds_and_serves_on_an_ephemeral_port() {
let out = with_server(1, |addr| async move {
assert_ne!(addr.port(), 0, "port 0 must resolve to a real port");
request(addr, GET).await.unwrap()
});
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
assert!(out.ends_with("hi"), "{out}");
}
#[test]
fn serves_concurrent_connections() {
let results = with_server(2, |addr| async move {
let mut set = tokio::task::JoinSet::new();
for _ in 0..64 {
set.spawn(async move { request(addr, GET).await });
}
let mut ok = 0;
while let Some(r) = set.join_next().await {
if r.unwrap().unwrap().starts_with("HTTP/1.1 200 OK") {
ok += 1;
}
}
ok
});
assert_eq!(results, 64);
}
#[test]
fn serves_across_multiple_workers() {
let results = with_server(4, |addr| async move {
let mut ok = 0;
for _ in 0..40 {
if request(addr, GET)
.await
.unwrap()
.starts_with("HTTP/1.1 200 OK")
{
ok += 1;
}
}
ok
});
assert_eq!(results, 40);
}
#[test]
fn single_worker_config_works() {
let out = with_server(1, |addr| async move { request(addr, GET).await.unwrap() });
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
}
#[test]
fn shutdown_stops_accepting() {
let server = Server::bind(test_config(1)).expect("bind");
let addr = server.local_addr();
let handle = server.handle();
let client = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let first = rt.block_on(request(addr, GET));
assert!(first.unwrap().starts_with("HTTP/1.1 200 OK"));
handle.shutdown();
assert!(handle.is_shutting_down());
handle.shutdown();
std::thread::sleep(Duration::from_millis(300));
rt.block_on(async {
match tokio::time::timeout(Duration::from_millis(500), request(addr, GET)).await {
Err(_) => true,
Ok(Err(_)) => true,
Ok(Ok(body)) => body.is_empty(),
}
})
});
server.serve(|| hello).expect("serve");
assert!(
client.join().unwrap(),
"no request may be served after shutdown"
);
}
#[test]
fn shutdown_before_serve_returns_immediately() {
let server = Server::bind(test_config(2)).expect("bind");
let handle = server.handle();
handle.shutdown();
assert!(handle.is_shutting_down());
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
server.serve(|| hello).expect("serve");
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(10)).is_ok(),
"serve must return when shutdown was signalled before it started"
);
}
#[test]
fn shutdown_after_serving_traffic_stops_every_worker() {
let server = Server::bind(test_config(4)).expect("bind");
let addr = server.local_addr();
let handle = server.handle();
let client = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut served = 0;
for _ in 0..8 {
if rt
.block_on(request(addr, GET))
.is_ok_and(|r| r.starts_with("HTTP/1.1 200 OK"))
{
served += 1;
}
}
handle.shutdown();
served
});
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
server.serve(|| hello).expect("serve");
let _ = tx.send(());
});
let served = client.join().expect("client thread");
assert_eq!(
served, 8,
"every request before the signal must have been answered, or the \
workers were never busy and the shutdown proves nothing"
);
assert!(
rx.recv_timeout(Duration::from_secs(10)).is_ok(),
"serve must return once every worker has stopped; a worker that \
missed the signal keeps accepting and join blocks forever"
);
}
struct SignalOnNthClone {
clones: Arc<AtomicUsize>,
at: usize,
handle: ServerHandle,
}
impl Clone for SignalOnNthClone {
fn clone(&self) -> Self {
if self.clones.fetch_add(1, Ordering::SeqCst) + 1 == self.at {
self.handle.shutdown();
}
Self {
clones: self.clones.clone(),
at: self.at,
handle: self.handle.clone(),
}
}
}
#[test]
fn shutdown_during_worker_startup_stops_every_worker() {
const WORKERS: usize = 8;
for at in 1..=WORKERS {
let server = Server::bind(test_config(WORKERS)).expect("bind");
let handle = server.handle();
let signal = SignalOnNthClone {
clones: Arc::new(AtomicUsize::new(0)),
at,
handle,
};
let clones = signal.clones.clone();
let (tx, rx) = std::sync::mpsc::channel();
let serving = std::thread::spawn(move || {
server
.serve(move || {
let _ = &signal;
hello
})
.expect("serve");
let _ = tx.send(());
});
assert!(
rx.recv_timeout(Duration::from_secs(10)).is_ok(),
"signalled from inside the spawn loop, before worker {at} of \
{WORKERS}: serve must return once every worker has stopped. A \
worker whose receiver was created after the signal never sees \
a transition, so it accepts forever and join blocks on it"
);
serving.join().expect("serve thread");
assert_eq!(
clones.load(Ordering::SeqCst),
WORKERS,
"the factory must be cloned once per worker, or the shutdown \
above was not fired from inside the spawn loop and this test \
is asserting nothing"
);
}
}
#[test]
fn transient_accept_errors_do_not_back_off() {
for kind in [
io::ErrorKind::ConnectionAborted,
io::ErrorKind::ConnectionReset,
io::ErrorKind::Interrupted,
io::ErrorKind::TimedOut,
] {
assert!(
!accept_backoff_warranted(&io::Error::from(kind)),
"{kind:?} is per-connection; pausing for it hands an attacker a \
throttle on the whole worker"
);
}
if let Some(eproto) = EPROTO {
assert!(
!accept_backoff_warranted(&io::Error::from_raw_os_error(eproto)),
"EPROTO is per-connection too, and no ErrorKind names it"
);
}
}
#[test]
fn resource_and_unknown_accept_errors_back_off() {
assert!(accept_backoff_warranted(&io::Error::from(
io::ErrorKind::OutOfMemory
)));
#[cfg(unix)]
for errno in [24, 23] {
let e = io::Error::from_raw_os_error(errno);
assert!(accept_backoff_warranted(&e));
}
assert!(accept_backoff_warranted(&io::Error::other("something new")));
}
#[test]
fn config_defaults_are_sane() {
let c = Config::new(loopback());
assert!(c.workers >= 1);
assert!(c.tcp.nodelay);
assert!(c.tcp.reuse_port);
assert_eq!(c.tick, Duration::from_millis(100));
assert_eq!(c.shutdown_grace, Duration::from_secs(10));
assert_eq!(Config::new(loopback()).workers(0).workers, 1, "never zero");
}
#[test]
fn limits_clamps_max_headers_to_the_parser_ceiling() {
let c = Config::new(loopback()).limits(Limits {
max_headers: 200,
..Default::default()
});
assert_eq!(c.limits.max_headers, crate::limits::MAX_HEADERS_CEILING);
assert_eq!(crate::limits::MAX_HEADERS_CEILING, 128);
}
#[test]
fn bind_reports_the_resolved_port() {
let s = Server::bind(test_config(3)).expect("bind");
let addr = s.local_addr();
assert_ne!(addr.port(), 0);
if let Listeners::PerWorker(v) = &s.listeners {
for l in v {
assert_eq!(l.local_addr().unwrap().port(), addr.port());
}
}
}
}