use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::Poll;
use std::time::{Duration, Instant};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
use tokio::task::JoinSet;
use fandhe_backend_http::body::BodyError;
use fandhe_backend_http::buffer::RecvBuffer;
use fandhe_backend_http::chunked::{ChunkedError, encode_chunk, encode_terminator};
use fandhe_backend_http::connection::{RequestError, read_request_with_limit, should_keep_alive};
use fandhe_backend_http::request::{HttpVersion, ParseError, RequestHead};
use fandhe_backend_http::response::Response;
use crate::extension::{GateContext, GateOutcome, Middleware, RequestGate, UpgradeHandler};
use crate::interceptor::Interceptor;
use crate::send_buffer::SendBuffer;
use crate::streaming::{RecvOutcome, StreamingResponse};
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
const FAR_FUTURE_FALLBACK: Duration = Duration::from_secs(30 * 24 * 60 * 60);
const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_MAX_CONNECTION_LIFETIME: Duration = Duration::from_secs(300);
const DEFAULT_MAX_REQUESTS_PER_CONNECTION: usize = 1_000;
const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(10);
const DEFAULT_SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(30);
const SHUTDOWN_NOTIFY_FLUSH_WAIT: Duration = Duration::from_millis(200);
const REBIND_BACKLOG_DRAIN_LIMIT: usize = 1024;
pub trait Handler: Send + Sync {
fn handle(&self, head: &RequestHead, body: &[u8]) -> fandhe_backend_routes::HandlerFuture;
fn handle_streaming(
&self,
_head: &RequestHead,
_body: &[u8],
) -> Option<crate::streaming::StreamingResponse> {
None
}
}
impl Handler for fandhe_backend_routes::Router {
fn handle(&self, head: &RequestHead, body: &[u8]) -> fandhe_backend_routes::HandlerFuture {
self.dispatch(head, body)
}
}
#[cfg(feature = "openapi")]
pub(crate) enum OpenApiRegistration {
Disabled,
Embedded,
Custom(fandhe_backend_plugin_openapi::OpenApiDoc),
}
pub struct Server {
middlewares: Vec<Box<dyn Middleware>>,
gates: Vec<Box<dyn RequestGate>>,
upgrade_handlers: Vec<Box<dyn UpgradeHandler>>,
interceptors: Vec<Box<dyn Interceptor>>,
handler: Option<Box<dyn Handler>>,
max_connections: usize,
max_connection_lifetime: Duration,
max_requests_per_connection: usize,
max_body_bytes: u64,
read_timeout: Duration,
keep_alive_enabled: bool,
shutdown_grace_period: Duration,
diagnostics: std::sync::Arc<dyn crate::diagnostics::Diagnostics>,
diagnostics_is_default: bool,
#[cfg(feature = "webrtc-proxy")]
webrtc_proxy_config: Option<fandhe_backend_plugin_webrtc_proxy::ProxyConfig>,
#[cfg(feature = "webrtc")]
webrtc_config: Option<fandhe_backend_plugin_webrtc::WebRtcConfig>,
#[cfg(feature = "websocket")]
websocket_configs: Vec<fandhe_backend_plugin_websocket::WebSocketConfig>,
#[cfg(feature = "graphql")]
graphql_config: Option<fandhe_backend_plugin_graphql::GraphQlConfig>,
#[cfg(feature = "openapi")]
openapi_registration: OpenApiRegistration,
#[cfg(feature = "cors")]
cors_config: Option<fandhe_backend_plugin_cors::CorsConfig>,
#[cfg(feature = "compression")]
compression_config: Option<fandhe_backend_plugin_compression::CompressionConfig>,
#[cfg(feature = "static")]
static_files_config: Option<fandhe_backend_plugin_static::StaticFilesConfig>,
}
impl Default for Server {
fn default() -> Self {
Self {
middlewares: Vec::new(),
gates: Vec::new(),
upgrade_handlers: Vec::new(),
interceptors: Vec::new(),
handler: None,
max_connections: DEFAULT_MAX_CONNECTIONS,
max_connection_lifetime: DEFAULT_MAX_CONNECTION_LIFETIME,
max_requests_per_connection: DEFAULT_MAX_REQUESTS_PER_CONNECTION,
max_body_bytes: fandhe_backend_http::body::MAX_BODY_BYTES,
read_timeout: DEFAULT_READ_TIMEOUT,
keep_alive_enabled: true,
shutdown_grace_period: DEFAULT_SHUTDOWN_GRACE_PERIOD,
diagnostics: std::sync::Arc::new(crate::diagnostics::StderrDiagnostics),
diagnostics_is_default: true,
#[cfg(feature = "webrtc-proxy")]
webrtc_proxy_config: None,
#[cfg(feature = "webrtc")]
webrtc_config: None,
#[cfg(feature = "websocket")]
websocket_configs: Vec::new(),
#[cfg(feature = "graphql")]
graphql_config: None,
#[cfg(feature = "openapi")]
openapi_registration: OpenApiRegistration::Disabled,
#[cfg(feature = "cors")]
cors_config: None,
#[cfg(feature = "compression")]
compression_config: None,
#[cfg(feature = "static")]
static_files_config: None,
}
}
}
impl Server {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
#[must_use]
pub fn max_connection_lifetime(mut self, max: Duration) -> Self {
self.max_connection_lifetime = max;
self
}
#[must_use]
pub fn max_requests_per_connection(mut self, max: usize) -> Self {
self.max_requests_per_connection = max;
self
}
#[must_use]
pub fn max_body_bytes(mut self, max: usize) -> Self {
self.max_body_bytes = max as u64;
self
}
#[must_use]
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
#[must_use]
pub fn keep_alive(mut self, enabled: bool) -> Self {
self.keep_alive_enabled = enabled;
self
}
#[must_use]
pub fn shutdown_grace_period(mut self, grace: Duration) -> Self {
self.shutdown_grace_period = grace;
self
}
#[must_use]
pub fn diagnostics(mut self, sink: impl crate::diagnostics::Diagnostics) -> Self {
self.diagnostics = std::sync::Arc::new(sink);
self.diagnostics_is_default = false;
self
}
#[must_use]
pub fn middleware(mut self, middleware: impl Middleware + 'static) -> Self {
self.middlewares.push(Box::new(middleware));
self
}
#[must_use]
pub fn gate(mut self, gate: impl RequestGate + 'static) -> Self {
self.gates.push(Box::new(gate));
self
}
#[must_use]
pub fn upgrade_handler(mut self, handler: impl UpgradeHandler + 'static) -> Self {
self.upgrade_handlers.push(Box::new(handler));
self
}
#[must_use]
pub fn interceptor(mut self, interceptor: impl Interceptor + 'static) -> Self {
self.interceptors.push(Box::new(interceptor));
self
}
#[must_use]
pub fn handler(mut self, handler: impl Handler + 'static) -> Self {
self.handler = Some(Box::new(handler));
self
}
#[cfg(feature = "webrtc-proxy")]
#[must_use]
pub fn webrtc_proxy(mut self, config: fandhe_backend_plugin_webrtc_proxy::ProxyConfig) -> Self {
self.webrtc_proxy_config = Some(config);
self
}
#[cfg(feature = "webrtc-proxy")]
pub(crate) fn webrtc_proxy_config(
&self,
) -> Option<&fandhe_backend_plugin_webrtc_proxy::ProxyConfig> {
self.webrtc_proxy_config.as_ref()
}
#[cfg(feature = "webrtc")]
#[must_use]
pub fn webrtc(mut self, config: fandhe_backend_plugin_webrtc::WebRtcConfig) -> Self {
self.webrtc_config = Some(config);
self
}
#[cfg(feature = "webrtc")]
pub(crate) fn webrtc_config(&self) -> Option<&fandhe_backend_plugin_webrtc::WebRtcConfig> {
self.webrtc_config.as_ref()
}
#[cfg(feature = "websocket")]
#[must_use]
pub fn websocket(mut self, config: fandhe_backend_plugin_websocket::WebSocketConfig) -> Self {
self.upgrade_handlers
.push(Box::new(WebSocketUpgradeAdapter {
config: config.clone(),
}));
self.websocket_configs.push(config);
self
}
#[cfg(feature = "websocket")]
pub(crate) fn websocket_configs(&self) -> &[fandhe_backend_plugin_websocket::WebSocketConfig] {
&self.websocket_configs
}
#[cfg(feature = "graphql")]
#[must_use]
pub fn graphql(mut self, config: fandhe_backend_plugin_graphql::GraphQlConfig) -> Self {
self.graphql_config = Some(config);
self
}
#[cfg(feature = "graphql")]
pub(crate) fn graphql_config(&self) -> Option<&fandhe_backend_plugin_graphql::GraphQlConfig> {
self.graphql_config.as_ref()
}
#[cfg(feature = "openapi")]
#[must_use]
pub fn openapi(mut self) -> Self {
self.openapi_registration = OpenApiRegistration::Embedded;
self
}
#[cfg(feature = "openapi")]
#[must_use]
pub fn openapi_with(mut self, doc: fandhe_backend_plugin_openapi::OpenApiDoc) -> Self {
self.openapi_registration = OpenApiRegistration::Custom(doc);
self
}
#[cfg(feature = "openapi")]
pub(crate) fn openapi_registration(&self) -> &OpenApiRegistration {
&self.openapi_registration
}
#[cfg(feature = "cors")]
#[must_use]
pub fn cors(mut self, config: fandhe_backend_plugin_cors::CorsConfig) -> Self {
self.cors_config = Some(config);
self
}
#[cfg(feature = "cors")]
pub(crate) fn cors_config(&self) -> Option<&fandhe_backend_plugin_cors::CorsConfig> {
self.cors_config.as_ref()
}
#[cfg(feature = "compression")]
#[must_use]
pub fn compression(
mut self,
config: fandhe_backend_plugin_compression::CompressionConfig,
) -> Self {
self.compression_config = Some(config);
self
}
#[cfg(feature = "compression")]
pub(crate) fn compression_config(
&self,
) -> Option<&fandhe_backend_plugin_compression::CompressionConfig> {
self.compression_config.as_ref()
}
#[cfg(feature = "static")]
#[must_use]
pub fn static_files(mut self, config: fandhe_backend_plugin_static::StaticFilesConfig) -> Self {
self.static_files_config = Some(config);
self
}
#[cfg(feature = "static")]
pub(crate) fn static_files_config(
&self,
) -> Option<&fandhe_backend_plugin_static::StaticFilesConfig> {
self.static_files_config.as_ref()
}
#[cfg(feature = "tracing")]
#[must_use]
pub fn tracing(mut self, config: fandhe_backend_plugin_tracing::TracingConfig) -> Self {
self.middlewares
.push(Box::new(TracingMiddleware::new(&config)));
self
}
pub async fn bind(self, addr: impl ToSocketAddrs) -> io::Result<BoundServer> {
let listener = TcpListener::bind(addr).await?;
let permit_count = self.max_connections.max(1);
let connection_limit = Arc::new(Semaphore::new(permit_count));
let permit_total = u32::try_from(permit_count).unwrap_or(u32::MAX);
Ok(BoundServer {
listener,
server: Arc::new(self),
connection_limit,
permit_total,
shutdown_flag: Arc::new(AtomicBool::new(false)),
rebind_tx: None,
rebind_rx: None,
})
}
}
#[cfg(feature = "websocket")]
struct WebSocketUpgradeAdapter {
config: fandhe_backend_plugin_websocket::WebSocketConfig,
}
#[cfg(feature = "websocket")]
impl UpgradeHandler for WebSocketUpgradeAdapter {
fn name(&self) -> &'static str {
"websocket"
}
fn matches(&self, head: &RequestHead) -> bool {
fandhe_backend_plugin_websocket::matches(head, &self.config)
}
}
#[cfg(feature = "tracing")]
struct TracingMiddleware {
layer: fandhe_backend_plugin_tracing::TracingLayer,
}
#[cfg(feature = "tracing")]
impl TracingMiddleware {
fn new(config: &fandhe_backend_plugin_tracing::TracingConfig) -> Self {
Self {
layer: fandhe_backend_plugin_tracing::TracingLayer::new(config),
}
}
}
#[cfg(feature = "tracing")]
impl Middleware for TracingMiddleware {
fn name(&self) -> &'static str {
"tracing"
}
fn on_request(&self, _head: &RequestHead) {
}
fn on_response(&self, head: &RequestHead, elapsed: Duration) {
self.layer.record_response(head, elapsed);
}
}
pub struct BoundServer {
listener: TcpListener,
server: Arc<Server>,
connection_limit: Arc<Semaphore>,
permit_total: u32,
shutdown_flag: Arc<AtomicBool>,
rebind_tx: Option<mpsc::Sender<RebindCommand>>,
rebind_rx: Option<mpsc::Receiver<RebindCommand>>,
}
struct RebindCommand {
listener: TcpListener,
reply: oneshot::Sender<()>,
}
#[derive(Clone)]
pub struct RebindHandle {
tx: mpsc::Sender<RebindCommand>,
}
impl RebindHandle {
pub async fn rebind(&self, addr: impl ToSocketAddrs) -> io::Result<SocketAddr> {
let listener = TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
let (reply_tx, reply_rx) = oneshot::channel();
self.tx
.send(RebindCommand {
listener,
reply: reply_tx,
})
.await
.map_err(|_| {
io::Error::other("rebind: BoundServer::run_until はすでに終了しています")
})?;
reply_rx.await.map_err(|_| {
io::Error::other("rebind: BoundServer::run_until が差し替え完了前に終了しました")
})?;
Ok(local_addr)
}
}
enum Raced<T> {
Shutdown,
Rebind(RebindCommand),
Completed(T),
}
async fn race_shutdown_or_accept<S, A>(
mut shutdown: Pin<&mut S>,
mut rebind_rx: Option<&mut mpsc::Receiver<RebindCommand>>,
accept: A,
) -> Raced<A::Output>
where
S: Future<Output = ()>,
A: Future,
{
let mut accept = std::pin::pin!(accept);
std::future::poll_fn(move |cx| {
if shutdown.as_mut().poll(cx).is_ready() {
return Poll::Ready(Raced::Shutdown);
}
if let Some(rx) = rebind_rx.as_mut()
&& let Poll::Ready(Some(cmd)) = rx.poll_recv(cx)
{
return Poll::Ready(Raced::Rebind(cmd));
}
match accept.as_mut().poll(cx) {
Poll::Ready(value) => Poll::Ready(Raced::Completed(value)),
Poll::Pending => Poll::Pending,
}
})
.await
}
struct CancelSafeJoinSet(JoinSet<()>);
impl std::ops::Deref for CancelSafeJoinSet {
type Target = JoinSet<()>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for CancelSafeJoinSet {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Drop for CancelSafeJoinSet {
fn drop(&mut self) {
self.0.detach_all();
}
}
fn spawn_generation_drain(
mut old_join_set: CancelSafeJoinSet,
old_cancel: crate::plugin::GenerationCancel,
session_drain: crate::plugin::SessionDrain,
grace: Duration,
diagnostics: std::sync::Arc<dyn crate::diagnostics::Diagnostics>,
) {
tokio::spawn(async move {
old_cancel.fire();
session_drain.fire(false);
let drained = tokio::time::timeout(grace, async {
while old_join_set.join_next().await.is_some() {}
})
.await;
if drained.is_err() {
old_join_set.shutdown().await;
crate::diagnostics::emit(
&*diagnostics,
crate::diagnostics::DiagnosticEvent::RebindDrainGraceExceeded { grace },
);
}
});
}
async fn drain_listener_backlog(
listener: &TcpListener,
connection_limit: &Arc<Semaphore>,
max: usize,
diagnostics: &dyn crate::diagnostics::Diagnostics,
) -> Vec<(TcpStream, SocketAddr, OwnedSemaphorePermit)> {
let mut drained = Vec::new();
for _ in 0..max {
let Ok(permit) = Arc::clone(connection_limit).try_acquire_owned() else {
break;
};
match std::future::poll_fn(|cx| Poll::Ready(listener.poll_accept(cx))).await {
Poll::Ready(Ok((stream, peer_addr))) => {
configure_accepted_stream(&stream, diagnostics);
drained.push((stream, peer_addr, permit));
}
Poll::Ready(Err(_)) | Poll::Pending => break,
}
}
drained
}
fn configure_accepted_stream(
stream: &TcpStream,
diagnostics: &dyn crate::diagnostics::Diagnostics,
) {
if let Err(err) = fandhe_backend_http::socket::configure_stream(stream) {
crate::diagnostics::emit(
diagnostics,
crate::diagnostics::DiagnosticEvent::TcpNodelayFailed { error: &err },
);
}
}
impl BoundServer {
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.listener.local_addr()
}
pub fn rebind_handle(&mut self) -> RebindHandle {
if let Some(tx) = &self.rebind_tx {
return RebindHandle { tx: tx.clone() };
}
let (tx, rx) = mpsc::channel(1);
self.rebind_tx = Some(tx.clone());
self.rebind_rx = Some(rx);
RebindHandle { tx }
}
pub async fn run(self) -> io::Result<()> {
self.run_until(std::future::pending::<()>()).await
}
pub async fn run_until<F>(self, shutdown: F) -> io::Result<()>
where
F: Future<Output = ()>,
{
let BoundServer {
mut listener,
server,
connection_limit,
permit_total,
shutdown_flag,
rebind_tx: _,
mut rebind_rx,
} = self;
let mut shutdown = std::pin::pin!(shutdown);
let mut join_set = CancelSafeJoinSet(JoinSet::new());
let mut current_shutdown_flag = shutdown_flag;
let mut current_cancel = crate::plugin::GenerationCancel::new();
let session_drain = crate::plugin::SessionDrain::new(&server);
loop {
while join_set.try_join_next().is_some() {}
let connection_limit_for_accept = Arc::clone(&connection_limit);
let accept_fut = async {
let permit = connection_limit_for_accept
.acquire_owned()
.await
.expect("connection_limit semaphore is never closed");
match listener.accept().await {
Ok((stream, peer_addr)) => {
configure_accepted_stream(&stream, &*server.diagnostics);
Some((stream, peer_addr, permit))
}
Err(err) => {
drop(permit);
crate::diagnostics::emit(
&*server.diagnostics,
crate::diagnostics::DiagnosticEvent::AcceptFailed { error: &err },
);
tokio::time::sleep(ACCEPT_ERROR_BACKOFF).await;
None
}
}
};
match race_shutdown_or_accept(shutdown.as_mut(), rebind_rx.as_mut(), accept_fut).await {
Raced::Shutdown => {
drop(rebind_rx.take());
break;
}
Raced::Rebind(RebindCommand {
listener: new_listener,
reply,
}) => {
current_shutdown_flag.store(true, Ordering::Relaxed);
for (stream, peer_addr, permit) in drain_listener_backlog(
&listener,
&connection_limit,
REBIND_BACKLOG_DRAIN_LIMIT,
&*server.diagnostics,
)
.await
{
let server = Arc::clone(&server);
let shutdown_flag = Arc::clone(¤t_shutdown_flag);
let cancel = current_cancel.handle();
join_set.spawn(async move {
handle_connection_with_permit(
&server,
stream,
Some(permit),
&shutdown_flag,
Some(peer_addr),
cancel,
)
.await;
});
}
listener = new_listener;
let old_join_set =
std::mem::replace(&mut join_set, CancelSafeJoinSet(JoinSet::new()));
let old_cancel = std::mem::replace(
&mut current_cancel,
crate::plugin::GenerationCancel::new(),
);
spawn_generation_drain(
old_join_set,
old_cancel,
session_drain.clone(),
server.shutdown_grace_period,
Arc::clone(&server.diagnostics),
);
current_shutdown_flag = Arc::new(AtomicBool::new(false));
let _ = reply.send(());
continue;
}
Raced::Completed(None) => continue,
Raced::Completed(Some((stream, peer_addr, permit))) => {
let server = Arc::clone(&server);
let shutdown_flag = Arc::clone(¤t_shutdown_flag);
let cancel = current_cancel.handle();
join_set.spawn(async move {
handle_connection_with_permit(
&server,
stream,
Some(permit),
&shutdown_flag,
Some(peer_addr),
cancel,
)
.await;
});
}
}
}
current_shutdown_flag.store(true, Ordering::Relaxed);
current_cancel.fire();
session_drain.fire(true);
drop(listener);
let drain_result = tokio::time::timeout(
server.shutdown_grace_period,
Arc::clone(&connection_limit).acquire_many_owned(permit_total),
)
.await;
match drain_result {
Ok(Ok(_permits)) => {
while join_set.join_next().await.is_some() {}
}
_ => {
join_set.shutdown().await;
let grace = server.shutdown_grace_period;
if server.diagnostics_is_default {
crate::diagnostics::emit(
&*server.diagnostics,
crate::diagnostics::DiagnosticEvent::ShutdownGraceExceeded { grace },
);
} else {
let diagnostics = Arc::clone(&server.diagnostics);
let (notified_tx, notified_rx) = oneshot::channel::<()>();
let _spawned = std::thread::Builder::new()
.name("fandhe-shutdown-diagnostics".to_owned())
.spawn(move || {
crate::diagnostics::emit(
&*diagnostics,
crate::diagnostics::DiagnosticEvent::ShutdownGraceExceeded {
grace,
},
);
let _ = notified_tx.send(());
});
let _ = tokio::time::timeout(SHUTDOWN_NOTIFY_FLUSH_WAIT, notified_rx).await;
}
}
}
Ok(())
}
}
pub async fn handle_connection<S>(server: &Server, stream: S)
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let no_shutdown = Arc::new(AtomicBool::new(false));
handle_connection_with_permit(
server,
stream,
None,
&no_shutdown,
None,
crate::plugin::UpgradeCancel::disabled(),
)
.await;
}
pub async fn handle_connection_with_peer_addr<S>(
server: &Server,
stream: S,
peer_addr: std::net::SocketAddr,
) where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let no_shutdown = Arc::new(AtomicBool::new(false));
handle_connection_with_permit(
server,
stream,
None,
&no_shutdown,
Some(peer_addr),
crate::plugin::UpgradeCancel::disabled(),
)
.await;
}
pub(crate) async fn handle_connection_with_permit<S>(
server: &Server,
mut stream: S,
mut permit: Option<OwnedSemaphorePermit>,
shutdown_flag: &Arc<AtomicBool>,
peer_addr: Option<std::net::SocketAddr>,
cancel: crate::plugin::UpgradeCancel,
) where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let gate_ctx = GateContext::new(peer_addr);
let mut buf = RecvBuffer::new();
let mut send_buf = SendBuffer::new();
let connection_started_at = Instant::now();
let mut requests_served: usize = 0;
let max_requests = server.max_requests_per_connection.max(1);
let sleep = tokio::time::sleep_until(tokio::time::Instant::now());
tokio::pin!(sleep);
loop {
let elapsed_since_start = connection_started_at.elapsed();
if elapsed_since_start >= server.max_connection_lifetime {
return;
}
let remaining_lifetime = server
.max_connection_lifetime
.saturating_sub(elapsed_since_start);
let read_timeout = server.read_timeout.min(remaining_lifetime);
let now = tokio::time::Instant::now();
let deadline = now
.checked_add(read_timeout)
.unwrap_or_else(|| now.checked_add(FAR_FUTURE_FALLBACK).unwrap_or(now));
sleep.as_mut().reset(deadline);
let read_result = {
let mut read_fut = std::pin::pin!(read_request_with_limit(
&mut stream,
&mut buf,
server.max_body_bytes
));
std::future::poll_fn(|cx| {
if let Poll::Ready(result) = read_fut.as_mut().poll(cx) {
return Poll::Ready(Ok(result));
}
if sleep.as_mut().poll(cx).is_ready() {
return Poll::Ready(Err(()));
}
Poll::Pending
})
.await
};
let request = match read_result {
Err(()) => return,
Ok(Err(err)) => {
if let Some(response) = error_response(&err) {
let _ = stream.write_all(&response.serialize(false)).await;
}
return;
}
Ok(Ok(None)) => return,
Ok(Ok(Some(request))) => request,
};
requests_served += 1;
let started_at = Instant::now();
for middleware in &server.middlewares {
middleware.on_request(&request.head);
}
let keep_alive = server.keep_alive_enabled
&& should_keep_alive(&request.head)
&& requests_served < max_requests
&& connection_started_at.elapsed() < server.max_connection_lifetime
&& !shutdown_flag.load(Ordering::Relaxed);
if let Some(rejection) = first_rejection(&server.gates, &request.head, &gate_ctx) {
let GateOutcome::Reject { response } = rejection else {
unreachable!("first_rejection only returns Reject outcomes")
};
if stream
.write_all(send_buf.serialize_response(&response, keep_alive))
.await
.is_err()
{
return;
}
send_buf.shrink_if_oversized();
for middleware in &server.middlewares {
middleware.on_response_with_status(
&request.head,
response.status,
started_at.elapsed(),
);
}
if !keep_alive {
return;
}
continue;
}
if server
.upgrade_handlers
.iter()
.any(|handler| handler.matches(&request.head))
{
if shutdown_flag.load(Ordering::Relaxed) {
let _ = stream
.write_all(&Response::empty(503).serialize(false))
.await;
return;
}
let leftover = buf.unread().to_vec();
drop(buf);
drop(send_buf);
match crate::plugin::try_handle_upgrade(
stream,
&request.head,
leftover,
server,
&mut permit,
cancel,
peer_addr,
)
.await
{
Some(mut stream) => {
let _ = stream
.write_all(&Response::empty(501).serialize(false))
.await;
return;
}
None => return,
}
}
let intercepted = server
.interceptors
.iter()
.find_map(|interceptor| interceptor.intercept(&request.head, &request.body));
let intercepted = match intercepted {
Some(response) => Some(response),
None => crate::plugin::try_intercept(server, &request.head, &request.body).await,
};
if intercepted.is_none()
&& let Some(handler) = &server.handler
&& let Some(streaming) = handler.handle_streaming(&request.head, &request.body)
{
let keep_alive_after = write_streaming_response(
&mut stream,
streaming,
&request.head,
keep_alive,
server,
connection_started_at,
shutdown_flag,
)
.await;
match keep_alive_after {
Some(StreamingCompletion {
keep_alive: keep_alive_after,
status,
}) => {
for middleware in &server.middlewares {
middleware.on_response_with_status(
&request.head,
status,
started_at.elapsed(),
);
}
if keep_alive_after {
continue;
}
return;
}
None => return,
}
}
let response = match intercepted {
Some(response) => response,
None => match &server.handler {
Some(handler) => handler.handle(&request.head, &request.body).await,
None => Response::empty(404),
},
};
let response = server
.interceptors
.iter()
.fold(response, |acc, interceptor| {
interceptor.map_response(&request.head, acc)
});
let response = crate::plugin::finalize_response(server, &request.head, response).await;
let keep_alive = keep_alive
&& connection_started_at.elapsed() < server.max_connection_lifetime
&& !shutdown_flag.load(Ordering::Relaxed);
if stream
.write_all(send_buf.serialize_response(&response, keep_alive))
.await
.is_err()
{
return;
}
send_buf.shrink_if_oversized();
for middleware in &server.middlewares {
middleware.on_response_with_status(
&request.head,
response.status,
started_at.elapsed(),
);
}
if !keep_alive {
return;
}
}
}
struct StreamingCompletion {
keep_alive: bool,
status: u16,
}
async fn write_streaming_response<S>(
stream: &mut S,
mut streaming: StreamingResponse,
head: &RequestHead,
keep_alive: bool,
server: &Server,
connection_started_at: Instant,
shutdown_flag: &Arc<AtomicBool>,
) -> Option<StreamingCompletion>
where
S: AsyncWrite + Unpin,
{
let remaining_lifetime = |started: Instant| -> Duration {
server
.max_connection_lifetime
.saturating_sub(started.elapsed())
};
let mut head_response = Response::empty(streaming.status);
if let Some(content_type) = streaming.content_type {
head_response = head_response.with_content_type(content_type);
}
head_response = server
.interceptors
.iter()
.fold(head_response, |acc, interceptor| {
interceptor.map_response(head, acc)
});
head_response = crate::plugin::finalize_streaming_head(server, head, head_response);
head_response.body = Vec::new();
let final_status = head_response.status;
if head.version == HttpVersion::Http10 {
let head_bytes = head_response.serialize_streaming_head_http10();
let write_timeout = DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
if tokio::time::timeout(write_timeout, stream.write_all(&head_bytes))
.await
.ok()?
.is_err()
{
return None;
}
if Response::is_bodyless_status(head_response.status) {
return Some(StreamingCompletion {
keep_alive: false,
status: final_status,
});
}
loop {
let recv_timeout = DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
let outcome = tokio::time::timeout(recv_timeout, streaming.recv())
.await
.ok()?;
match outcome {
RecvOutcome::Chunk(data) => {
if data.is_empty() {
continue;
}
let write_timeout =
DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
if tokio::time::timeout(write_timeout, stream.write_all(&data))
.await
.ok()?
.is_err()
{
return None;
}
}
RecvOutcome::End => {
return Some(StreamingCompletion {
keep_alive: false,
status: final_status,
});
}
RecvOutcome::Aborted => return None,
}
}
}
let keep_alive = keep_alive
&& connection_started_at.elapsed() < server.max_connection_lifetime
&& !shutdown_flag.load(Ordering::Relaxed);
let (head_response, mut body_encoder) =
crate::plugin::prepare_streaming_compression(server, head, head_response);
let head_bytes = head_response.serialize_chunked_head(keep_alive);
let write_timeout = DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
if tokio::time::timeout(write_timeout, stream.write_all(&head_bytes))
.await
.ok()?
.is_err()
{
return None;
}
if Response::is_bodyless_status(head_response.status) {
return Some(StreamingCompletion {
keep_alive: keep_alive
&& connection_started_at.elapsed() < server.max_connection_lifetime
&& !shutdown_flag.load(Ordering::Relaxed),
status: final_status,
});
}
loop {
let recv_timeout = DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
let outcome = tokio::time::timeout(recv_timeout, streaming.recv())
.await
.ok()?;
match outcome {
RecvOutcome::Chunk(data) => {
let data = body_encoder.transform(data)?;
let mut framed = Vec::with_capacity(data.len() + 16);
encode_chunk(&data, &mut framed);
if framed.is_empty() {
continue;
}
let write_timeout =
DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
if tokio::time::timeout(write_timeout, stream.write_all(&framed))
.await
.ok()?
.is_err()
{
return None;
}
}
RecvOutcome::End => {
let residual = body_encoder.finish()?;
let mut terminator = Vec::new();
if !residual.is_empty() {
encode_chunk(&residual, &mut terminator);
}
encode_terminator(&mut terminator);
let write_timeout =
DEFAULT_WRITE_TIMEOUT.min(remaining_lifetime(connection_started_at));
if tokio::time::timeout(write_timeout, stream.write_all(&terminator))
.await
.ok()?
.is_err()
{
return None;
}
return Some(StreamingCompletion {
keep_alive: keep_alive
&& connection_started_at.elapsed() < server.max_connection_lifetime
&& !shutdown_flag.load(Ordering::Relaxed),
status: final_status,
});
}
RecvOutcome::Aborted => return None,
}
}
}
fn first_rejection(
gates: &[Box<dyn RequestGate>],
head: &RequestHead,
ctx: &GateContext,
) -> Option<GateOutcome> {
gates.iter().find_map(|gate| match gate.check(head, ctx) {
GateOutcome::Allow => None,
reject @ GateOutcome::Reject { .. } => Some(reject),
})
}
fn error_response(err: &RequestError) -> Option<Response> {
match err {
RequestError::Parse(ParseError::HeaderSectionTooLarge | ParseError::TooManyHeaders) => {
Some(Response::empty(431))
}
RequestError::Parse(ParseError::InvalidRequestLine | ParseError::InvalidHeader) => {
Some(Response::empty(400))
}
RequestError::Parse(ParseError::UnsupportedVersion) => Some(Response::empty(505)),
RequestError::Body(BodyError::BodyTooLarge) => Some(Response::empty(413)),
RequestError::Body(BodyError::TransferEncodingUnsupported) => Some(Response::empty(501)),
RequestError::Body(
BodyError::DuplicateContentLength
| BodyError::InvalidContentLength
| BodyError::ContentLengthWithChunked,
) => Some(Response::empty(400)),
RequestError::Chunked(ChunkedError::BodyTooLarge) => Some(Response::empty(413)),
RequestError::Chunked(
ChunkedError::InvalidChunkSize
| ChunkedError::ChunkLineTooLong
| ChunkedError::TooManyChunks
| ChunkedError::TrailerUnsupported
| ChunkedError::InvalidLineTerminator,
) => Some(Response::empty(400)),
RequestError::UnexpectedEof | RequestError::Io(_) => None,
}
}
#[allow(dead_code)]
fn _assert_handle_connection_accepts_tcp_stream(
server: &Server,
stream: TcpStream,
) -> impl Future + '_ {
handle_connection(server, stream)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extension::{GateContext, GateOutcome, Middleware, RequestGate, UpgradeHandler};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::AsyncReadExt;
struct RecordingMiddleware {
events: Mutex<Vec<&'static str>>,
}
impl Middleware for RecordingMiddleware {
fn name(&self) -> &'static str {
"recording"
}
fn on_request(&self, _head: &RequestHead) {
self.events.lock().unwrap().push("on_request");
}
fn on_response(&self, _head: &RequestHead, _elapsed: Duration) {
self.events.lock().unwrap().push("on_response");
}
}
struct StatusRecordingMiddleware {
statuses: Mutex<Vec<u16>>,
}
impl Middleware for StatusRecordingMiddleware {
fn name(&self) -> &'static str {
"status-recording"
}
fn on_request(&self, _head: &RequestHead) {}
fn on_response_with_status(&self, _head: &RequestHead, status: u16, _elapsed: Duration) {
self.statuses.lock().unwrap().push(status);
}
}
struct StatusProxy(Arc<StatusRecordingMiddleware>);
impl Middleware for StatusProxy {
fn name(&self) -> &'static str {
"status-proxy"
}
fn on_request(&self, _head: &RequestHead) {}
fn on_response_with_status(&self, head: &RequestHead, status: u16, elapsed: Duration) {
self.0.on_response_with_status(head, status, elapsed);
}
}
struct RequireAuthGate;
impl RequestGate for RequireAuthGate {
fn name(&self) -> &'static str {
"require-auth"
}
fn check(&self, head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
match head.header("authorization") {
Some(_) => GateOutcome::Allow,
None => GateOutcome::reject(401, b"unauthorized".to_vec()),
}
}
}
struct AlwaysRejectGate(u16);
impl RequestGate for AlwaysRejectGate {
fn name(&self) -> &'static str {
"always-reject"
}
fn check(&self, _head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
GateOutcome::reject(self.0, Vec::new())
}
}
struct RateLimitGate;
impl RequestGate for RateLimitGate {
fn name(&self) -> &'static str {
"rate-limit"
}
fn check(&self, _head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
let response = Response::new(429, b"{\"error\":\"rate limited\"}".to_vec())
.with_content_type("application/json")
.with_header("Retry-After", "30")
.expect("リテラル値は構築時検証を通る");
GateOutcome::Reject { response }
}
}
struct AllowAllGate;
impl RequestGate for AllowAllGate {
fn name(&self) -> &'static str {
"allow-all"
}
fn check(&self, _head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
GateOutcome::Allow
}
}
struct RecordingPeerAddrGate {
observed: std::sync::Mutex<Vec<Option<std::net::SocketAddr>>>,
}
impl RequestGate for RecordingPeerAddrGate {
fn name(&self) -> &'static str {
"recording-peer-addr"
}
fn check(&self, _head: &RequestHead, ctx: &GateContext) -> GateOutcome {
self.observed.lock().unwrap().push(ctx.peer_addr());
GateOutcome::Allow
}
}
impl RequestGate for Arc<RecordingPeerAddrGate> {
fn name(&self) -> &'static str {
(**self).name()
}
fn check(&self, head: &RequestHead, ctx: &GateContext) -> GateOutcome {
(**self).check(head, ctx)
}
}
struct WebSocketUpgrade;
impl UpgradeHandler for WebSocketUpgrade {
fn name(&self) -> &'static str {
"websocket"
}
fn matches(&self, head: &RequestHead) -> bool {
head.header("upgrade")
.is_some_and(|v| v.eq_ignore_ascii_case("websocket"))
}
}
struct FixedHandler {
status: u16,
body: &'static [u8],
calls: AtomicUsize,
}
impl Handler for FixedHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready({
self.calls.fetch_add(1, Ordering::Relaxed);
Response::new(self.status, self.body.to_vec())
}))
}
}
async fn roundtrip(server: &Server, request: &[u8]) -> String {
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
client.write_all(request).await.unwrap();
client.shutdown().await.unwrap();
handle_connection(server, server_stream).await;
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
String::from_utf8(out).unwrap()
}
async fn roundtrip_with_shutdown_flag(server: &Server, request: &[u8]) -> String {
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
client.write_all(request).await.unwrap();
client.shutdown().await.unwrap();
let shutdown_flag = Arc::new(AtomicBool::new(true));
handle_connection_with_permit(
server,
server_stream,
None,
&shutdown_flag,
None,
crate::plugin::UpgradeCancel::disabled(),
)
.await;
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
String::from_utf8(out).unwrap()
}
#[tokio::test]
async fn no_handler_registered_returns_404() {
let server = Server::new();
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 404 Not Found\r\n"));
}
#[tokio::test]
async fn registered_handler_is_invoked() {
let handler = FixedHandler {
status: 200,
body: b"hi",
calls: AtomicUsize::new(0),
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.ends_with("hi"));
}
#[tokio::test]
async fn router_registered_as_handler_dispatches_by_method_and_target() {
let router = fandhe_backend_routes::Router::new().route("GET", "/", |_head, _body| {
Response::new(200, b"root".to_vec())
});
let server = Server::new().handler(router);
let ok = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(ok.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(ok.contains("Content-Length: 4\r\n"));
assert!(ok.contains("Connection: close\r\n"));
assert!(ok.ends_with("root"));
let missing = roundtrip(
&server,
b"GET /missing HTTP/1.1\r\nConnection: close\r\n\r\n",
)
.await;
assert!(missing.starts_with("HTTP/1.1 404 Not Found\r\n"));
assert!(missing.contains("Content-Length: 0\r\n"));
let wrong_method =
roundtrip(&server, b"POST / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(wrong_method.starts_with("HTTP/1.1 405 Method Not Allowed\r\n"));
assert!(wrong_method.contains("Content-Length: 0\r\n"));
assert!(wrong_method.contains("Allow: GET\r\n"));
}
#[tokio::test]
async fn router_registered_as_handler_dispatches_path_params() {
let router = fandhe_backend_routes::Router::new()
.route_param("GET", "/hello/{name}", |_head, params, _body| {
let name = params.get("name").unwrap_or("world");
Response::new(200, format!("hello, {name}").into_bytes())
})
.expect("valid pattern");
let server = Server::new().handler(router);
let ok = roundtrip(
&server,
b"GET /hello/alice HTTP/1.1\r\nConnection: close\r\n\r\n",
)
.await;
assert!(ok.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(ok.ends_with("hello, alice"));
}
#[tokio::test]
async fn middleware_hooks_fire_in_order_with_positive_elapsed() {
let mw = Arc::new(RecordingMiddleware {
events: Mutex::new(Vec::new()),
});
struct MwProxy(Arc<RecordingMiddleware>);
impl Middleware for MwProxy {
fn name(&self) -> &'static str {
"proxy"
}
fn on_request(&self, head: &RequestHead) {
self.0.on_request(head);
}
fn on_response(&self, head: &RequestHead, elapsed: Duration) {
self.0.on_response(head, elapsed);
}
}
let server = Server::new().middleware(MwProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
let events = mw.events.lock().unwrap();
assert_eq!(*events, vec!["on_request", "on_response"]);
}
#[tokio::test]
async fn gate_rejects_missing_authorization_with_401() {
let server = Server::new().gate(RequireAuthGate);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 401 Unauthorized\r\n"));
assert!(response.ends_with("unauthorized"));
}
#[tokio::test]
async fn gate_allows_request_with_authorization_to_reach_handler() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().gate(RequireAuthGate).handler(handler);
let response = roundtrip(
&server,
b"GET / HTTP/1.1\r\nAuthorization: Bearer x\r\nConnection: close\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
}
#[tokio::test]
async fn first_gate_rejection_wins_over_later_gates() {
let server = Server::new()
.gate(AlwaysRejectGate(403))
.gate(AlwaysRejectGate(401));
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 403 Forbidden\r\n"));
}
#[tokio::test]
async fn gate_reject_with_headers_reaches_the_wire() {
let server = Server::new().gate(RateLimitGate);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 429 Too Many Requests\r\n"));
assert!(response.contains("Retry-After: 30\r\n"));
assert!(response.contains("Content-Type: application/json\r\n"));
assert!(response.contains("Content-Length: "));
assert!(response.contains("Connection: close\r\n"));
assert!(response.ends_with("{\"error\":\"rate limited\"}"));
}
#[tokio::test]
async fn allow_all_gate_does_not_block_handler() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().gate(AllowAllGate).handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
}
#[tokio::test]
async fn upgrade_match_returns_501_and_closes_without_invoking_handler() {
let handler = FixedHandler {
status: 200,
body: b"should-not-be-called",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.upgrade_handler(WebSocketUpgrade)
.handler(handler);
let response = roundtrip(
&server,
b"GET /ws HTTP/1.1\r\nUpgrade: websocket\r\nConnection: keep-alive\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 501 Not Implemented\r\n"));
}
#[tokio::test]
async fn non_matching_upgrade_handler_falls_through_to_handler() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.upgrade_handler(WebSocketUpgrade)
.handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
}
#[tokio::test]
async fn upgrade_match_returns_503_when_shutdown_flag_is_set() {
let handler = FixedHandler {
status: 200,
body: b"should-not-be-called",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.upgrade_handler(WebSocketUpgrade)
.handler(handler);
let response = roundtrip_with_shutdown_flag(
&server,
b"GET /ws HTTP/1.1\r\nUpgrade: websocket\r\nConnection: keep-alive\r\n\r\n",
)
.await;
assert!(
response.starts_with("HTTP/1.1 503 Service Unavailable\r\n"),
"shutdown 後の Upgrade は 503 で拒否されるはず(実際: {response})"
);
}
#[tokio::test]
async fn keep_alive_request_gets_connection_close_when_shutdown_flag_is_set() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().handler(handler);
let response = roundtrip_with_shutdown_flag(
&server,
b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(
response.to_lowercase().contains("connection: close"),
"shutdown 後は keep-alive 要求でも Connection: close を返すはず\
(実際: {response})"
);
}
#[tokio::test]
async fn keep_alive_connection_serves_two_requests_sequentially() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let write_task = tokio::spawn(async move {
client
.write_all(b"GET /a HTTP/1.1\r\n\r\nGET /b HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
out
});
handle_connection(&server, server_stream).await;
let out = write_task.await.unwrap();
let text = String::from_utf8(out).unwrap();
assert_eq!(text.matches("HTTP/1.1 200 OK").count(), 2);
}
#[tokio::test]
async fn max_requests_per_connection_forces_close_after_limit() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.max_requests_per_connection(2)
.handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let write_task = tokio::spawn(async move {
client
.write_all(
b"GET /a HTTP/1.1\r\n\r\n\
GET /b HTTP/1.1\r\n\r\n\
GET /c HTTP/1.1\r\n\r\n",
)
.await
.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
out
});
handle_connection(&server, server_stream).await;
let out = write_task.await.unwrap();
let text = String::from_utf8(out).unwrap();
assert_eq!(text.matches("HTTP/1.1 200 OK").count(), 2);
assert!(text.contains("Connection: close"));
}
#[tokio::test]
async fn keep_alive_disabled_forces_close_after_first_request() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().keep_alive(false).handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let write_task = tokio::spawn(async move {
client
.write_all(b"GET /a HTTP/1.1\r\n\r\nGET /b HTTP/1.1\r\n\r\n")
.await
.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
out
});
handle_connection(&server, server_stream).await;
let out = write_task.await.unwrap();
let text = String::from_utf8(out).unwrap();
assert_eq!(text.matches("HTTP/1.1 200 OK").count(), 1);
assert!(text.contains("Connection: close"));
}
#[tokio::test]
async fn max_connection_lifetime_closes_before_next_read() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.max_connection_lifetime(Duration::from_millis(0))
.handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let write_task = tokio::spawn(async move {
let _ = client.write_all(b"GET / HTTP/1.1\r\n\r\n").await;
let mut out = Vec::new();
let _ = client.read_to_end(&mut out).await;
out
});
handle_connection(&server, server_stream).await;
let out = write_task.await.unwrap();
assert!(out.is_empty());
}
#[tokio::test]
async fn max_connection_lifetime_bounds_slow_read_below_read_timeout() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.max_connection_lifetime(Duration::from_millis(50))
.handler(handler);
let (client, server_stream) = tokio::io::duplex(8192);
let started = Instant::now();
handle_connection(&server, server_stream).await;
let elapsed = started.elapsed();
drop(client);
assert!(
elapsed < Duration::from_secs(5),
"生存期間の上限近辺で閉じられるはずが {elapsed:?} かかった(read_timeout 固定に戻っていないか確認)"
);
}
#[tokio::test]
async fn read_timeout_closes_slow_client_at_configured_value() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.read_timeout(Duration::from_millis(50))
.handler(handler);
let (client, server_stream) = tokio::io::duplex(8192);
let started = Instant::now();
handle_connection(&server, server_stream).await;
let elapsed = started.elapsed();
drop(client);
assert!(
elapsed < Duration::from_secs(5),
"read_timeout(50ms) を設定したのに {elapsed:?} かかった(既定 30 秒のまま反映されていないか確認)"
);
}
#[tokio::test]
async fn read_timeout_zero_fails_closed() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().read_timeout(Duration::ZERO).handler(handler);
let (client, server_stream) = tokio::io::duplex(8192);
let started = Instant::now();
handle_connection(&server, server_stream).await;
let elapsed = started.elapsed();
drop(client);
assert!(
elapsed < Duration::from_secs(5),
"read_timeout(ZERO) を設定したのに {elapsed:?} かかった(フェイルクローズできていない)"
);
}
#[tokio::test]
async fn extreme_read_timeout_is_bounded_by_connection_lifetime() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.read_timeout(Duration::from_secs(86_400))
.max_connection_lifetime(Duration::from_millis(50))
.handler(handler);
let (client, server_stream) = tokio::io::duplex(8192);
let started = Instant::now();
handle_connection(&server, server_stream).await;
let elapsed = started.elapsed();
drop(client);
assert!(
elapsed < Duration::from_secs(5),
"read_timeout を極端に大きくしても max_connection_lifetime の上限で \
閉じられるはずが {elapsed:?} かかった(read_timeout が生存期間で \
丸められていないか確認)"
);
}
#[tokio::test]
async fn slow_client_partial_send_times_out_mid_read() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.read_timeout(Duration::from_millis(100))
.handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
client.write_all(b"GET / HTTP/1.1\r\n").await.unwrap();
let started = Instant::now();
handle_connection(&server, server_stream).await;
let elapsed = started.elapsed();
drop(client);
assert!(
elapsed < Duration::from_secs(5),
"部分送信後に停止したクライアントが read_timeout(100ms) 近辺で \
閉じられるはずが {elapsed:?} かかった(reset 後のタイムアウトが \
発火していない可能性)"
);
}
#[tokio::test]
async fn keep_alive_reset_survives_multiple_requests_below_timeout() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.read_timeout(Duration::from_secs(1))
.handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let client_task = tokio::spawn(async move {
for _ in 0..2 {
client
.write_all(b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n")
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(700)).await;
}
client
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
client.shutdown().await.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
String::from_utf8(out).unwrap()
});
handle_connection(&server, server_stream).await;
let out = client_task.await.unwrap();
assert_eq!(
out.matches("HTTP/1.1 200").count(),
3,
"reset 漏れにより誤タイムアウトが起きた可能性(3 リクエストとも \
200 応答されるはずが実際の応答: {out:?})"
);
}
#[tokio::test]
async fn request_arriving_near_timeout_deadline_is_processed() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new()
.read_timeout(Duration::from_secs(1))
.handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
let client_task = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(700)).await;
client
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
client.shutdown().await.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
String::from_utf8(out).unwrap()
});
handle_connection(&server, server_stream).await;
let out = client_task.await.unwrap();
assert!(
out.starts_with("HTTP/1.1 200"),
"read_timeout の 70% 経過後に届いたリクエストは正常応答される \
はずが、タイムアウト扱いで応答なしクローズされた(実際の応答: {out:?})"
);
}
struct SlowHandler {
sleep_for: Duration,
}
impl Handler for SlowHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready({
std::thread::sleep(self.sleep_for);
Response::new(200, b"ok".to_vec())
}))
}
}
#[tokio::test]
async fn keep_alive_becomes_close_when_lifetime_expires_during_handle() {
let lifetime = Duration::from_millis(30);
let handler = SlowHandler {
sleep_for: lifetime + Duration::from_millis(50),
};
let server = Server::new()
.max_connection_lifetime(lifetime)
.handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(
response.contains("Connection: close"),
"handle 中に生存期間を超過したにもかかわらず keep-alive のまま応答した: {response:?}"
);
}
#[tokio::test]
async fn invalid_request_line_returns_400() {
let server = Server::new();
let response = roundtrip(&server, b"G@T / HTTP/1.1\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 400 Bad Request\r\n"));
}
#[tokio::test]
async fn too_many_headers_returns_431() {
let server = Server::new();
let mut request = b"GET / HTTP/1.1\r\n".to_vec();
for i in 0..200 {
request.extend_from_slice(format!("X-{i}: v\r\n").as_bytes());
}
request.extend_from_slice(b"\r\n");
let response = roundtrip(&server, &request).await;
assert!(response.starts_with("HTTP/1.1 431 Request Header Fields Too Large\r\n"));
}
#[tokio::test]
async fn body_too_large_returns_413() {
let server = Server::new();
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nContent-Length: 999999999999\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
}
#[tokio::test]
async fn transfer_encoding_gzip_returns_501() {
let server = Server::new();
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 501 Not Implemented\r\n"));
}
#[tokio::test]
async fn transfer_encoding_chunked_is_accepted_and_dispatched() {
let server = Server::new();
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n0\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 404 Not Found\r\n"));
}
#[tokio::test]
async fn content_length_with_chunked_returns_400() {
let server = Server::new();
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n4\r\nWiki\r\n0\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 400 Bad Request\r\n"));
}
#[tokio::test]
async fn chunked_invalid_size_returns_400() {
let server = Server::new();
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nZZZ\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 400 Bad Request\r\n"));
}
#[tokio::test]
async fn chunked_body_too_large_returns_413() {
use fandhe_backend_http::body::MAX_BODY_BYTES;
let server = Server::new();
let request = format!(
"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n{:X}\r\n",
MAX_BODY_BYTES + 1
);
let response = roundtrip(&server, request.as_bytes()).await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
}
#[tokio::test]
async fn max_body_bytes_custom_limit_rejects_over_boundary() {
let server = Server::new().max_body_bytes(4);
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\nabcde",
)
.await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
}
#[tokio::test]
async fn max_body_bytes_custom_limit_accepts_at_boundary() {
let server = Server::new().max_body_bytes(4);
let response =
roundtrip(&server, b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nabcd").await;
assert!(response.starts_with("HTTP/1.1 404 Not Found\r\n"));
}
#[tokio::test]
async fn max_body_bytes_custom_limit_propagates_to_chunked() {
let server = Server::new().max_body_bytes(4);
let response = roundtrip(
&server,
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nabcde\r\n0\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
}
#[tokio::test]
async fn max_body_bytes_zero_rejects_body_but_accepts_bodyless() {
let server = Server::new().max_body_bytes(0);
let response = roundtrip(&server, b"POST / HTTP/1.1\r\nContent-Length: 1\r\n\r\nx").await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
let server = Server::new().max_body_bytes(0);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 404 Not Found\r\n"));
}
#[tokio::test]
async fn max_body_bytes_default_matches_unmodified_server() {
use fandhe_backend_http::body::MAX_BODY_BYTES;
let server = Server::new();
let request = format!(
"POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
MAX_BODY_BYTES + 1
);
let response = roundtrip(&server, request.as_bytes()).await;
assert!(response.starts_with("HTTP/1.1 413 Payload Too Large\r\n"));
}
#[tokio::test]
async fn immediate_eof_closes_without_response() {
let server = Server::new();
let (client, server_stream) = tokio::io::duplex(64);
drop(client);
handle_connection(&server, server_stream).await;
}
#[tokio::test]
async fn max_connections_limits_concurrent_accept() {
use tokio::time::timeout;
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let server = Server::new().max_connections(1).handler(handler);
let bound = server.bind("127.0.0.1:0").await.unwrap();
let addr = bound.local_addr().unwrap();
tokio::spawn(async move {
let _ = bound.run().await;
});
let client1 = TcpStream::connect(addr).await.unwrap();
let mut client2 = TcpStream::connect(addr).await.unwrap();
client2
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut probe = [0u8; 1];
let no_response_yet = timeout(Duration::from_millis(200), client2.read(&mut probe)).await;
assert!(
no_response_yet.is_err(),
"max_connections が守られていれば client2 はまだ応答を受け取らないはず"
);
drop(client1);
let mut out = Vec::new();
timeout(Duration::from_secs(2), client2.read_to_end(&mut out))
.await
.expect("permit 解放後は client2 が受理されるはず")
.unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.starts_with("HTTP/1.1 200 OK\r\n"));
}
#[tokio::test]
async fn request_gate_receives_real_peer_addr_over_tcp() {
let gate = Arc::new(RecordingPeerAddrGate {
observed: std::sync::Mutex::new(Vec::new()),
});
let server = Server::new().gate(Arc::clone(&gate));
let bound = server.bind("127.0.0.1:0").await.unwrap();
let addr = bound.local_addr().unwrap();
tokio::spawn(async move {
let _ = bound.run().await;
});
let mut client = TcpStream::connect(addr).await.unwrap();
let client_local_addr = client.local_addr().unwrap();
client
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
let observed = gate.observed.lock().unwrap();
assert_eq!(
observed.as_slice(),
&[Some(client_local_addr)],
"gate が観測した peer_addr はクライアントの local_addr と一致するはず"
);
}
#[tokio::test]
async fn handle_connection_duplex_path_yields_none_peer_addr() {
let gate = Arc::new(RecordingPeerAddrGate {
observed: std::sync::Mutex::new(Vec::new()),
});
let server = Server::new().gate(Arc::clone(&gate));
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
client
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
client.shutdown().await.unwrap();
handle_connection(&server, server_stream).await;
let observed = gate.observed.lock().unwrap();
assert_eq!(observed.as_slice(), &[None]);
}
#[tokio::test]
async fn handle_connection_with_peer_addr_injects_supplied_addr() {
let gate = Arc::new(RecordingPeerAddrGate {
observed: std::sync::Mutex::new(Vec::new()),
});
let server = Server::new().gate(Arc::clone(&gate));
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::AsyncWriteExt as _;
client
.write_all(b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
client.shutdown().await.unwrap();
let injected_addr: std::net::SocketAddr = "203.0.113.9:9999".parse().unwrap();
handle_connection_with_peer_addr(&server, server_stream, injected_addr).await;
let observed = gate.observed.lock().unwrap();
assert_eq!(observed.as_slice(), &[Some(injected_addr)]);
}
#[test]
fn bound_server_run_accepts_tcp_stream_type() {
fn _type_check() {
let _ = _assert_handle_connection_accepts_tcp_stream;
}
}
struct StreamingHandler {
status: u16,
content_type: Option<&'static str>,
chunks: Vec<&'static [u8]>,
}
impl Handler for StreamingHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(
&self,
_head: &RequestHead,
_body: &[u8],
) -> Option<crate::streaming::StreamingResponse> {
let (response, writer) =
crate::streaming::StreamingResponse::channel(self.status, self.content_type, 4);
let chunks: Vec<Vec<u8>> = self.chunks.iter().map(|c| c.to_vec()).collect();
tokio::spawn(async move {
for chunk in chunks {
if writer.send(chunk).await.is_err() {
return;
}
}
let _ = writer.finish().await;
});
Some(response)
}
}
struct AbortingStreamingHandler {
chunks: Vec<&'static [u8]>,
}
impl Handler for AbortingStreamingHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(
&self,
_head: &RequestHead,
_body: &[u8],
) -> Option<crate::streaming::StreamingResponse> {
let (response, writer) = crate::streaming::StreamingResponse::channel(200, None, 4);
let chunks: Vec<Vec<u8>> = self.chunks.iter().map(|c| c.to_vec()).collect();
tokio::spawn(async move {
for chunk in chunks {
if writer.send(chunk).await.is_err() {
return;
}
}
});
Some(response)
}
}
#[tokio::test]
async fn streaming_handler_sends_chunked_framing_and_terminator() {
let handler = StreamingHandler {
status: 200,
content_type: Some("text/plain"),
chunks: vec![b"foo", b"bar"],
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("Transfer-Encoding: chunked\r\n"));
assert!(response.contains("Content-Type: text/plain\r\n"));
assert!(!response.contains("Content-Length"));
assert!(response.contains("3\r\nfoo\r\n"));
assert!(response.contains("3\r\nbar\r\n"));
assert!(response.ends_with("0\r\n\r\n"));
}
#[tokio::test]
async fn handler_without_streaming_override_keeps_content_length_response() {
let handler = FixedHandler {
status: 200,
body: b"hi",
calls: AtomicUsize::new(0),
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.contains("Content-Length: 2\r\n"));
assert!(!response.contains("Transfer-Encoding"));
}
#[tokio::test]
async fn streaming_response_without_finish_closes_without_terminator() {
let handler = AbortingStreamingHandler {
chunks: vec![b"partial"],
};
let server = Server::new().handler(handler);
let response =
roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("7\r\npartial\r\n"));
assert!(!response.ends_with("0\r\n\r\n"));
}
#[tokio::test]
async fn streaming_response_continues_keep_alive_for_pipelined_next_request() {
let handler = StreamingHandler {
status: 200,
content_type: None,
chunks: vec![b"ok"],
};
let server = Server::new().handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
client
.write_all(b"GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
handle_connection(&server, server_stream).await;
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
let text = String::from_utf8(out).unwrap();
let occurrences = text.matches("0\r\n\r\n").count();
assert_eq!(
occurrences, 2,
"keep-alive 継続によりパイプライン済み 2 リクエスト目にも応答するはず: {text:?}"
);
}
#[tokio::test]
async fn streaming_bodyless_status_omits_transfer_encoding_and_terminator() {
let handler = StreamingHandler {
status: 204,
content_type: None,
chunks: vec![b"leaked"],
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 204 No Content\r\n"));
assert!(!response.contains("Transfer-Encoding"));
assert!(!response.contains("Content-Length"));
assert!(
!response.contains("leaked"),
"body を持ち得ないステータスでは producer が送った chunk が \
ワイヤへ漏れてはならない: {response:?}"
);
assert!(response.ends_with("\r\n\r\n"));
}
#[tokio::test]
async fn streaming_bodyless_status_keeps_keep_alive_without_desync() {
let handler = StreamingHandler {
status: 204,
content_type: None,
chunks: vec![],
};
let server = Server::new().handler(handler);
let (mut client, server_stream) = tokio::io::duplex(8192);
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
client
.write_all(b"GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nConnection: close\r\n\r\n")
.await
.unwrap();
handle_connection(&server, server_stream).await;
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
let text = String::from_utf8(out).unwrap();
let expected =
"HTTP/1.1 204 No Content\r\n\r\nHTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n";
assert_eq!(
text, expected,
"204 応答 2 連続はヘッド 2 個のみで構成され、チャンク終端の \
混入による desync があってはならない"
);
}
#[tokio::test]
async fn http10_streaming_bodyless_status_omits_body_and_framing_headers() {
let handler = StreamingHandler {
status: 304,
content_type: None,
chunks: vec![b"leaked"],
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.0\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 304 Not Modified\r\n"));
assert!(response.contains("Connection: close\r\n"));
assert!(!response.contains("Transfer-Encoding"));
assert!(!response.contains("Content-Length"));
assert!(
!response.contains("leaked"),
"body を持ち得ないステータスでは producer が送った chunk が \
ワイヤへ漏れてはならない: {response:?}"
);
assert!(response.ends_with("\r\n\r\n"));
}
#[tokio::test]
async fn streaming_backpressure_send_blocks_when_channel_is_full() {
let (_response, writer) = crate::streaming::StreamingResponse::channel(200, None, 1);
writer.send(b"first".to_vec()).await.unwrap();
let send_result = tokio::time::timeout(
std::time::Duration::from_millis(100),
writer.send(b"second".to_vec()),
)
.await;
assert!(
send_result.is_err(),
"容量超過時、受信側が読み出さない限り send は完了しないはず"
);
}
#[tokio::test]
async fn http10_streaming_request_gets_connection_close_and_eof_terminated_body() {
let handler = StreamingHandler {
status: 200,
content_type: None,
chunks: vec![b"hello"],
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.0\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("Connection: close\r\n"));
assert!(!response.contains("Transfer-Encoding"));
assert!(!response.contains("Content-Length"));
assert!(response.ends_with("hello"));
}
#[tokio::test]
async fn streaming_keep_alive_request_gets_connection_close_when_shutdown_flag_is_set() {
let handler = StreamingHandler {
status: 200,
content_type: None,
chunks: vec![b"ok"],
};
let server = Server::new().handler(handler);
let response = roundtrip_with_shutdown_flag(
&server,
b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n",
)
.await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(
response.contains("Connection: close\r\n"),
"shutdown_flag が立っていれば chunked ヘッドも Connection: close を \
広告するはず(実際: {response:?})"
);
}
#[tokio::test]
async fn streaming_abort_does_not_invoke_on_response() {
let handler = AbortingStreamingHandler {
chunks: vec![b"partial"],
};
let mw = Arc::new(RecordingMiddleware {
events: Mutex::new(Vec::new()),
});
struct MwProxy(Arc<RecordingMiddleware>);
impl Middleware for MwProxy {
fn name(&self) -> &'static str {
"proxy"
}
fn on_request(&self, head: &RequestHead) {
self.0.on_request(head);
}
fn on_response(&self, head: &RequestHead, elapsed: Duration) {
self.0.on_response(head, elapsed);
}
}
let server = Server::new()
.handler(handler)
.middleware(MwProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n").await;
let events = mw.events.lock().unwrap();
assert_eq!(
*events,
vec!["on_request"],
"打ち切り(finish なし drop)では on_response を呼ばないはず: {events:?}"
);
}
#[tokio::test]
async fn http10_streaming_abort_does_not_invoke_on_response() {
let handler = AbortingStreamingHandler {
chunks: vec![b"partial"],
};
let mw = Arc::new(RecordingMiddleware {
events: Mutex::new(Vec::new()),
});
struct MwProxy(Arc<RecordingMiddleware>);
impl Middleware for MwProxy {
fn name(&self) -> &'static str {
"proxy"
}
fn on_request(&self, head: &RequestHead) {
self.0.on_request(head);
}
fn on_response(&self, head: &RequestHead, elapsed: Duration) {
self.0.on_response(head, elapsed);
}
}
let server = Server::new()
.handler(handler)
.middleware(MwProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.0\r\n\r\n").await;
let events = mw.events.lock().unwrap();
assert_eq!(
*events,
vec!["on_request"],
"HTTP/1.0 でも打ち切り(finish なし drop)では on_response を \
呼ばないはず: {events:?}"
);
}
struct AddHeaderInterceptor {
name: &'static str,
header: (&'static str, &'static str),
}
impl Interceptor for AddHeaderInterceptor {
fn name(&self) -> &'static str {
self.name
}
fn map_response(&self, _head: &RequestHead, response: Response) -> Response {
response
.with_header(self.header.0.to_string(), self.header.1.to_string())
.expect("test header は検証済み固定値")
}
}
struct BodyInjectingInterceptor;
impl Interceptor for BodyInjectingInterceptor {
fn name(&self) -> &'static str {
"body-injecting"
}
fn map_response(&self, _head: &RequestHead, mut response: Response) -> Response {
response.body = b"INJECTED".to_vec();
response
}
}
struct RewriteToNoContentInterceptor;
impl Interceptor for RewriteToNoContentInterceptor {
fn name(&self) -> &'static str {
"rewrite-to-204"
}
fn map_response(&self, _head: &RequestHead, mut response: Response) -> Response {
response.status = 204;
response
}
}
#[tokio::test]
async fn streaming_map_response_adds_header_to_chunked_head() {
let handler = StreamingHandler {
status: 200,
content_type: Some("text/plain"),
chunks: vec![b"foo", b"bar"],
};
let server = Server::new()
.handler(handler)
.interceptor(AddHeaderInterceptor {
name: "add-header",
header: ("X-Mapped", "1"),
});
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("X-Mapped: 1\r\n"));
assert!(response.contains("Transfer-Encoding: chunked\r\n"));
assert!(response.contains("3\r\nfoo\r\n"));
assert!(response.contains("3\r\nbar\r\n"));
assert!(response.ends_with("0\r\n\r\n"));
}
#[tokio::test]
async fn streaming_map_response_body_is_discarded() {
let handler = StreamingHandler {
status: 200,
content_type: None,
chunks: vec![b"real-chunk"],
};
let server = Server::new()
.handler(handler)
.interceptor(BodyInjectingInterceptor);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(!response.contains("INJECTED"));
assert!(response.contains("a\r\nreal-chunk\r\n"));
assert!(response.ends_with("0\r\n\r\n"));
}
#[tokio::test]
async fn streaming_map_response_status_rewrite_to_bodyless_suppresses_framing() {
let handler = StreamingHandler {
status: 200,
content_type: None,
chunks: vec![b"unreachable"],
};
let server = Server::new()
.handler(handler)
.interceptor(RewriteToNoContentInterceptor);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 204 No Content\r\n"));
assert!(!response.contains("Transfer-Encoding"));
assert!(!response.contains("unreachable"));
assert!(response.ends_with("\r\n\r\n"));
assert!(!response.trim_end_matches("\r\n\r\n").ends_with('0'));
}
#[tokio::test]
async fn streaming_map_response_applies_on_http10_head() {
let handler = StreamingHandler {
status: 200,
content_type: Some("text/plain"),
chunks: vec![b"foo"],
};
let server = Server::new()
.handler(handler)
.interceptor(AddHeaderInterceptor {
name: "add-header-http10",
header: ("X-Mapped-10", "1"),
});
let response = roundtrip(&server, b"GET / HTTP/1.0\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("X-Mapped-10: 1\r\n"));
assert!(response.contains("Connection: close\r\n"));
assert!(!response.contains("Transfer-Encoding"));
assert!(response.ends_with("foo"));
}
#[tokio::test]
async fn streaming_without_interceptor_is_unaffected_by_map_response_wiring() {
let handler = StreamingHandler {
status: 200,
content_type: Some("text/plain"),
chunks: vec![b"foo", b"bar"],
};
let server = Server::new().handler(handler);
let response = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(response.contains("Transfer-Encoding: chunked\r\n"));
assert!(response.contains("Content-Type: text/plain\r\n"));
assert!(response.contains("3\r\nfoo\r\n"));
assert!(response.contains("3\r\nbar\r\n"));
assert!(response.ends_with("0\r\n\r\n"));
}
async fn listener_with_backlog(n: usize) -> TcpListener {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind は成功する");
let addr = listener.local_addr().expect("local_addr は成功する");
for _ in 0..n {
let _ = TcpStream::connect(addr).await.expect("connect は成功する");
}
tokio::time::sleep(Duration::from_millis(50)).await;
listener
}
#[tokio::test]
async fn drain_backlog_collects_pending_connections() {
let listener = listener_with_backlog(3).await;
let connection_limit = Arc::new(Semaphore::new(10));
let drained = drain_listener_backlog(
&listener,
&connection_limit,
10,
&crate::diagnostics::StderrDiagnostics,
)
.await;
assert_eq!(drained.len(), 3);
assert_eq!(connection_limit.available_permits(), 7);
for (stream, _, _) in &drained {
assert!(
stream.nodelay().expect("nodelay() は成功する"),
"drain_listener_backlog が回収した接続は TCP_NODELAY が有効であるべき"
);
}
}
#[tokio::test]
async fn drain_backlog_stops_at_permit_exhaustion() {
let listener = listener_with_backlog(2).await;
let connection_limit = Arc::new(Semaphore::new(1));
let drained = drain_listener_backlog(
&listener,
&connection_limit,
10,
&crate::diagnostics::StderrDiagnostics,
)
.await;
assert_eq!(drained.len(), 1);
assert_eq!(connection_limit.available_permits(), 0);
}
#[tokio::test]
async fn drain_backlog_stops_at_max_cap() {
let listener = listener_with_backlog(2).await;
let connection_limit = Arc::new(Semaphore::new(10));
let drained = drain_listener_backlog(
&listener,
&connection_limit,
1,
&crate::diagnostics::StderrDiagnostics,
)
.await;
assert_eq!(drained.len(), 1);
assert_eq!(connection_limit.available_permits(), 9);
}
#[tokio::test]
async fn drain_backlog_returns_empty_when_no_pending() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind は成功する");
let connection_limit = Arc::new(Semaphore::new(10));
let drained = tokio::time::timeout(
Duration::from_millis(200),
drain_listener_backlog(
&listener,
&connection_limit,
10,
&crate::diagnostics::StderrDiagnostics,
),
)
.await
.expect("backlog 空でもタイムアウトせず即座に返る");
assert!(drained.is_empty());
assert_eq!(connection_limit.available_permits(), 10);
}
#[tokio::test]
async fn configure_accepted_stream_enables_nodelay() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind は成功する");
let addr = listener.local_addr().expect("local_addr は成功する");
let accept_task = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept は成功する");
configure_accepted_stream(&stream, &crate::diagnostics::StderrDiagnostics);
assert!(
stream.nodelay().expect("nodelay() は成功する"),
"configure_accepted_stream 適用後は TCP_NODELAY が有効であるべき"
);
});
let _client = TcpStream::connect(addr).await.expect("connect は成功する");
accept_task.await.expect("accept task は panic しない");
}
#[tokio::test]
async fn middleware_observes_status_for_normal_response() {
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![200]);
}
#[tokio::test]
async fn middleware_observes_404_when_no_handler() {
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new().middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![404]);
}
#[tokio::test]
async fn middleware_observes_gate_reject_status() {
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.gate(RequireAuthGate)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![401]);
}
#[tokio::test]
async fn middleware_observes_status_after_interceptor_map_response() {
struct RewriteTo418;
impl Interceptor for RewriteTo418 {
fn name(&self) -> &'static str {
"rewrite-to-418"
}
fn map_response(&self, _head: &RequestHead, mut response: Response) -> Response {
response.status = 418;
response
}
}
let handler = FixedHandler {
status: 200,
body: b"ok",
calls: AtomicUsize::new(0),
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.interceptor(RewriteTo418)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![418]);
}
#[tokio::test]
async fn middleware_observes_streaming_status() {
let handler = StreamingHandler {
status: 201,
content_type: None,
chunks: vec![b"ok"],
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![201]);
}
#[tokio::test]
async fn middleware_observes_streaming_status_on_http10() {
let handler = StreamingHandler {
status: 201,
content_type: None,
chunks: vec![b"ok"],
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.0\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![201]);
}
#[tokio::test]
async fn middleware_observes_streaming_bodyless_status() {
let handler = StreamingHandler {
status: 204,
content_type: None,
chunks: vec![],
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n").await;
assert_eq!(*mw.statuses.lock().unwrap(), vec![204]);
}
#[tokio::test]
async fn streaming_abort_does_not_invoke_on_response_with_status() {
let handler = AbortingStreamingHandler {
chunks: vec![b"partial"],
};
let mw = Arc::new(StatusRecordingMiddleware {
statuses: Mutex::new(Vec::new()),
});
let server = Server::new()
.handler(handler)
.middleware(StatusProxy(Arc::clone(&mw)));
let _ = roundtrip(&server, b"GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n").await;
assert!(
mw.statuses.lock().unwrap().is_empty(),
"打ち切り(finish なし drop)では on_response_with_status を \
呼ばないはず: {:?}",
mw.statuses.lock().unwrap()
);
}
}