v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 20:34:13 +02:00
1 parent 9b3d6731cc
commit 00f29f92c8
68 files changed
+2599 -513

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
// HTTP/1 dispatcher and upgrade flow; split from TLS listener runtime.
#[allow(clippy::too_many_arguments)]
async fn connection<I>(
stream: I,
router: Router,
peer: SocketAddr,
local: SocketAddr,
transport: Transport,
tls_sni: Option<String>,
h2: bool,
extended_connect: bool,
h2c_upgrade: bool,
frontend_state: Option<Arc<App>>,
lease: Arc<ConnectionLease>,
stop: CancellationToken,
grace: Duration,
http2_settings: Http2Connections,
sendfile_slots: TransportSendfileSlots,
) where
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
if h2 {
http2_connection(
stream,
router,
peer,
local,
transport,
tls_sni.clone(),
extended_connect,
frontend_state.clone(),
lease,
stop,
grace,
http2_settings,
)
.await;
return;
}
let request_stop = stop.clone();
let service_frontend_state = frontend_state.clone();
let service = service_fn(move |mut request: Request<Incoming>| {
let router = router.clone();
let request_stop = request_stop.clone();
let request_lease = lease.clone();
let upgrade_state = service_frontend_state.clone();
#[cfg(target_os = "linux")]
let sendfile_slots = sendfile_slots.clone();
async move {
request.extensions_mut().insert(PeerAddr(peer));
request.extensions_mut().insert(LocalAddr(local));
request.extensions_mut().insert(transport);
request.extensions_mut().insert(request_lease.clone());
#[cfg(target_os = "linux")]
if let Some(slots) = sendfile_slots {
request.extensions_mut().insert(slots);
}
request
.extensions_mut()
.insert(ServerShutdown(request_stop.clone()));
if h2c_attempt(&request) {
if !h2c_upgrade {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_rejected",
"h2c upgrade is disabled on this listener",
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from("h2c upgrade is disabled on this listener"))
.expect("static h2c disabled response"),
);
}
let injected_headers = match h2_headers_frame(&request) {
Ok(frame) => frame,
Err(message) => {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_invalid",
message,
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from(message))
.expect("static h2c validation response"),
);
}
};
let on_upgrade = hyper::upgrade::on(&mut request);
let upgrade_router = router.clone();
let upgrade_stop = request_stop.clone();
let upgrade_lease = request_lease.clone();
let upgrade_state_for_task = upgrade_state.clone();
let task = async move {
match on_upgrade.await {
Ok(upgraded) => {
if let Some(state) = &upgrade_state_for_task {
state
.metrics
.http2_connections
.fetch_add(1, Ordering::Relaxed);
}
let extended_connect =
upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
http2_connection(
io,
upgrade_router,
peer,
local,
Transport { tls: false },
None,
extended_connect,
upgrade_state_for_task.clone(),
upgrade_lease,
upgrade_stop,
grace,
http2_settings,
)
.await;
}
Err(error) => {
log_protocol_error(
upgrade_state_for_task.as_ref(),
"h2c_transport_error",
&error,
peer,
local,
"tcp",
"2",
0,
None,
);
tracing::debug!(%error, "h2c upgrade transport failed");
}
}
};
if let Some(state) = upgrade_state {
state.tunnels.spawn(task);
} else {
tokio::spawn(task);
}
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::SWITCHING_PROTOCOLS)
.header(http::header::CONNECTION, "Upgrade")
.header(http::header::UPGRADE, "h2c")
.body(Body::empty())
.expect("static h2c switching response"),
);
}
router.oneshot(request.map(Body::new)).await
}
});
let mut builder = http1::Builder::new();
builder
.timer(TokioTimer::new())
.keep_alive(true)
.max_headers(64)
.max_buf_size(65_536)
.header_read_timeout(Duration::from_secs(10));
let conn = builder
.serve_connection(TokioIo::new(stream), service)
.with_upgrades();
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(error) = result {
log_protocol_error(
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
);
tracing::debug!(%error, "HTTP/1 connection closed");
}
},
_ = stop.cancelled() => {
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
},
}
}
+301
View File
@@ -0,0 +1,301 @@
//! Linux HTTP/1 kernel sendfile bridge.
//!
//! Hyper's Body uses Bytes, not file descriptors. This opt-in bridge conveys a
//! uniquely tagged, exact-length placeholder through the ordinary metering
//! pipeline; a TCP-only AsyncWrite adapter replaces those bytes with the
//! original, already-validated file via Linux sendfile(2). An unmatched marker
//! or short file is a hard I/O error, never silently returned as user content.
//! Never register a placeholder for cached, compressed, transformed, TLS, H2,
//! H3, ranged or non-GET responses.
use bytes::Bytes;
use http_body::{Body as HttpBody, Frame, SizeHint};
use std::{
fs::File,
io,
os::fd::{AsRawFd, RawFd},
pin::Pin,
sync::{Arc, Mutex},
sync::atomic::Ordering,
task::{Context, Poll},
};
use tokio::{io::{AsyncRead, AsyncWrite, Interest, ReadBuf}, net::TcpStream};
const TAG_LEN: usize = 32;
const FRAME_LEN: usize = 64 * 1024;
const MAX_PENDING: usize = 8;
const MIN_SIZE: u64 = 64 * 1024;
#[derive(Clone, Debug)]
pub(crate) struct SendfileCandidate {
pub file: Arc<File>,
pub length: u64,
}
struct Slot {
tag: [u8; TAG_LEN],
file: Arc<File>,
length: u64,
written: u64,
}
#[derive(Default)]
struct Registry {
slots: Vec<Slot>,
active: Option<[u8; TAG_LEN]>,
}
#[derive(Clone, Default)]
pub(crate) struct SendfileSlots(Arc<Mutex<Registry>>);
pub(crate) struct SendfileBody {
bytes: Bytes,
remaining: u64,
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
}
// Web Server counters count emitted body frames before any downstream encoder.
// Atomic reference is supplied by the Web Server that originally opened the file.
impl SendfileSlots {
pub fn create_body(
&self,
candidate: SendfileCandidate,
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
) -> Option<SendfileBody> {
if !(MIN_SIZE..=i64::MAX as u64).contains(&candidate.length) {
return None;
}
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
if registry.slots.len() >= MAX_PENDING { return None; }
let mut tag = [0u8; TAG_LEN];
tag[0] = 0; // A valid HTTP/1 response header never contains NUL.
tag[1..].copy_from_slice(&rand::random::<[u8; TAG_LEN - 1]>());
if registry.slots.iter().any(|slot| slot.tag == tag) { return None; }
let mut data = vec![0u8; FRAME_LEN];
for chunk in data.chunks_exact_mut(TAG_LEN) { chunk.copy_from_slice(&tag); }
registry.slots.push(Slot {
tag, file: candidate.file, length: candidate.length, written: 0,
});
Some(SendfileBody {
bytes: Bytes::from(data), remaining: candidate.length, web_metrics,
})
}
fn plan(&self, buf: &[u8]) -> WritePlan {
let registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
// Critical for mixed reverse-proxy traffic: almost all socket writes
// have no pending static file; do not inspect their payload at all.
if registry.slots.is_empty() { return WritePlan::Plain(buf.len()); }
if let Some(tag) = registry.active {
if let Some(slot) = registry.slots.iter().find(|s| s.tag == tag) {
return plan_file(slot, buf);
}
return WritePlan::Invalid;
}
// A marker is an opaque 32-byte random tag, searched only on a
// connection that has a registered response. Headers preceding a
// marker are written intact; each subsequent body byte is validated.
for start in 0..buf.len() {
if buf[start] != 0 { continue; }
for slot in &registry.slots {
let available = buf.len() - start;
let compare = available.min(TAG_LEN);
if buf[start..start + compare] == slot.tag[..compare] {
if start > 0 { return WritePlan::Plain(start); }
return plan_file(slot, buf);
}
}
}
WritePlan::Plain(buf.len())
}
fn commit(&self, tag: [u8; TAG_LEN], count: usize) {
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
registry.active = Some(tag);
if let Some(slot) = registry.slots.iter_mut().find(|s| s.tag == tag) {
slot.written += count as u64;
if slot.written == slot.length {
registry.slots.retain(|s| s.tag != tag);
registry.active = None;
}
}
}
}
enum WritePlan {
Plain(usize),
File { tag: [u8; TAG_LEN], fd: RawFd, file: Arc<File>, offset: u64, count: usize },
Invalid,
}
fn plan_file(slot: &Slot, buf: &[u8]) -> WritePlan {
let expected = (slot.length - slot.written).min(buf.len() as u64) as usize;
let matched = buf[..expected].iter().enumerate()
.take_while(|(n, byte)| **byte == slot.tag[(slot.written as usize + n) % TAG_LEN])
.count();
if matched == 0 { return WritePlan::Invalid; }
WritePlan::File {
tag: slot.tag, fd: slot.file.as_raw_fd(), file: slot.file.clone(),
offset: slot.written, count: matched,
}
}
impl HttpBody for SendfileBody {
type Data = Bytes;
type Error = io::Error;
fn poll_frame(mut self: Pin<&mut Self>, _cx: &mut Context<'_>)
-> Poll<Option<Result<Frame<Bytes>, io::Error>>>
{
if self.remaining == 0 { return Poll::Ready(None); }
let len = self.remaining.min(FRAME_LEN as u64) as usize;
self.remaining -= len as u64;
self.web_metrics.response_bytes.fetch_add(len as u64, Ordering::Relaxed);
Poll::Ready(Some(Ok(Frame::data(self.bytes.slice(0..len)))))
}
fn size_hint(&self) -> SizeHint { SizeHint::with_exact(self.remaining) }
fn is_end_stream(&self) -> bool { self.remaining == 0 }
}
pub(crate) struct SendfileIo {
stream: TcpStream,
slots: SendfileSlots,
}
impl SendfileIo {
pub fn new(stream: TcpStream) -> (Self, SendfileSlots) {
let slots = SendfileSlots::default();
(Self { stream, slots: slots.clone() }, slots)
}
}
impl AsyncRead for SendfileIo {
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
}
}
impl AsyncWrite for SendfileIo {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
if buf.is_empty() { return Poll::Ready(Ok(0)); }
match self.slots.plan(buf) {
WritePlan::Plain(count) => Pin::new(&mut self.stream).poll_write(cx, &buf[..count]),
WritePlan::Invalid => Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData,
"sendfile placeholder did not match its registered descriptor"))),
WritePlan::File { tag, fd, file, offset, count } => {
let _keep_open = file;
let socket: RawFd = self.stream.as_raw_fd();
let mut at: libc::off_t = offset as libc::off_t;
match self.stream.poll_write_ready(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Ready(Ok(())) => {
let result = self.stream.try_io(Interest::WRITABLE, || {
let n = unsafe { libc::sendfile(socket, fd, &mut at, count) };
if n < 0 { Err(io::Error::last_os_error()) }
else if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof,
"static source truncated during sendfile")) }
else { Ok(n as usize) }
});
match result {
Ok(n) => { self.slots.commit(tag, n); Poll::Ready(Ok(n)) },
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
Err(e) => Poll::Ready(Err(e)),
}
}
}
}
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_shutdown(cx)
}
fn is_write_vectored(&self) -> bool { self.stream.is_write_vectored() }
fn poll_write_vectored(mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>]) -> Poll<io::Result<usize>> {
// Preserve zero-extra-syscall vectored I/O whenever no sendfile body
// has been prepared on this connection.
if self.slots.0.lock().unwrap_or_else(|e| e.into_inner()).slots.is_empty() {
return Pin::new(&mut self.stream).poll_write_vectored(cx, bufs);
}
match bufs.iter().find(|part| !part.is_empty()) {
Some(part) => self.poll_write(cx, part),
None => Poll::Ready(Ok(0)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body::Body;
use tempfile::tempfile;
use std::io::Write;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn placeholder_has_exact_frame_lengths_and_recovers_fd() {
let mut file = tempfile().unwrap();
file.write_all(&vec![42u8; 100_000]).unwrap();
let slots = SendfileSlots::default();
let counter = Arc::new(crate::web_server::metrics::PhpMetrics::default());
let mut body = slots.create_body(SendfileCandidate { file: Arc::new(file), length: 100_000 }, counter.clone()).unwrap();
let first = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
};
assert_eq!(first.len(), FRAME_LEN);
match slots.plan(&first) { WritePlan::File { count, .. } => assert_eq!(count, FRAME_LEN), _ => panic!("missing kernel file plan") }
let tag = slots.0.lock().unwrap().slots[0].tag;
slots.commit(tag, FRAME_LEN);
let second = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
};
assert_eq!(second.len(), 34_464);
match slots.plan(&second) { WritePlan::File { count, offset, .. } => { assert_eq!(offset, 65_536); assert_eq!(count, 34_464); }, _ => panic!("second plan missing") }
slots.commit(tag, second.len());
assert!(slots.0.lock().unwrap().slots.is_empty());
assert!(body.is_end_stream());
assert_eq!(counter.response_bytes.load(Ordering::Relaxed), 100_000);
}
#[tokio::test]
async fn native_sendfile_transfers_exact_file_and_keeps_socket_reusable() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let (mut socket, slots) = SendfileIo::new(tcp);
for round in 0..2 {
let expected = if round == 0 { vec![0, 1, 2, 3].repeat(25_000) }
else { vec![9u8; 100_003] };
let mut file = tempfile().unwrap();
file.write_all(&expected).unwrap();
let web = Arc::new(crate::web_server::metrics::PhpMetrics::default());
let body = slots.create_body(SendfileCandidate {
file: Arc::new(file), length: expected.len() as u64,
}, web.clone()).unwrap();
let first = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len());
// Combine header and first marker into one socket write: the
// bridge must send the header before enabling kernel transfer.
let mut body = body;
let first_frame = http_body_util::BodyExt::frame(&mut body).await.unwrap().unwrap().into_data().unwrap();
let mut joined = first.into_bytes();
joined.extend_from_slice(&first_frame);
socket.write_all(&joined).await.unwrap();
while let Some(frame) = http_body_util::BodyExt::frame(&mut body).await {
socket.write_all(&frame.unwrap().into_data().unwrap()).await.unwrap();
}
assert_eq!(web.response_bytes.load(Ordering::Relaxed), expected.len() as u64);
}
socket.shutdown().await.unwrap();
});
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
let mut actual = Vec::new();
tokio::time::timeout(std::time::Duration::from_secs(10), client.read_to_end(&mut actual)).await.unwrap().unwrap();
server.await.unwrap();
let mut wanted = Vec::new();
for expected in [vec![0, 1, 2, 3].repeat(25_000), vec![9u8; 100_003]] {
wanted.extend_from_slice(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len()).as_bytes());
wanted.extend_from_slice(&expected);
}
assert_eq!(actual, wanted, "kernel sendfile must never leak placeholder markers");
}
}
+20 -199
View File
@@ -134,6 +134,7 @@ async fn serve_inner(
stop,
grace,
http2_settings,
Default::default(),
)
.await;
},
@@ -178,6 +179,7 @@ async fn serve_inner(
stop,
grace,
Http2Connections::default(),
Default::default(),
)
.await;
return;
@@ -208,6 +210,7 @@ async fn serve_inner(
stop,
grace,
Http2Connections::default(),
Default::default(),
)
.await;
}
@@ -242,6 +245,22 @@ async fn serve_inner(
&& frontend_state
.as_ref()
.is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false));
// The existing Hyper transport remains untouched when the
// option is disabled. Connections opened after a hot config
// change acquire a new bridge automatically.
#[cfg(target_os = "linux")]
if !prior_h2 && frontend_state.as_ref().is_some_and(|state| {
state.config.load().web_php_services.values()
.any(|service| service.sendfile == crate::web_server::SendfileMode::Auto)
}) {
let (io, slots) = sendfile::SendfileIo::new(stream);
connection(
io, router, peer, local, Transport { tls: false }, None,
false, false, h2c, frontend_state, lease, stop, grace,
http2_settings, Some(slots),
).await;
return;
}
connection(
stream,
router,
@@ -257,6 +276,7 @@ async fn serve_inner(
stop,
grace,
http2_settings,
Default::default(),
)
.await;
}
@@ -273,205 +293,6 @@ async fn serve_inner(
while tasks.join_next().await.is_some() {}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn connection<I>(
stream: I,
router: Router,
peer: SocketAddr,
local: SocketAddr,
transport: Transport,
tls_sni: Option<String>,
h2: bool,
extended_connect: bool,
h2c_upgrade: bool,
frontend_state: Option<Arc<App>>,
lease: Arc<ConnectionLease>,
stop: CancellationToken,
grace: Duration,
http2_settings: Http2Connections,
) where
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
if h2 {
http2_connection(
stream,
router,
peer,
local,
transport,
tls_sni.clone(),
extended_connect,
frontend_state.clone(),
lease,
stop,
grace,
http2_settings,
)
.await;
return;
}
let request_stop = stop.clone();
let service_frontend_state = frontend_state.clone();
let service = service_fn(move |mut request: Request<Incoming>| {
let router = router.clone();
let request_stop = request_stop.clone();
let request_lease = lease.clone();
let upgrade_state = service_frontend_state.clone();
async move {
request.extensions_mut().insert(PeerAddr(peer));
request.extensions_mut().insert(LocalAddr(local));
request.extensions_mut().insert(transport);
request.extensions_mut().insert(request_lease.clone());
request
.extensions_mut()
.insert(ServerShutdown(request_stop.clone()));
if h2c_attempt(&request) {
if !h2c_upgrade {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_rejected",
"h2c upgrade is disabled on this listener",
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from("h2c upgrade is disabled on this listener"))
.expect("static h2c disabled response"),
);
}
let injected_headers = match h2_headers_frame(&request) {
Ok(frame) => frame,
Err(message) => {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_invalid",
message,
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from(message))
.expect("static h2c validation response"),
);
}
};
let on_upgrade = hyper::upgrade::on(&mut request);
let upgrade_router = router.clone();
let upgrade_stop = request_stop.clone();
let upgrade_lease = request_lease.clone();
let upgrade_state_for_task = upgrade_state.clone();
let task = async move {
match on_upgrade.await {
Ok(upgraded) => {
if let Some(state) = &upgrade_state_for_task {
state
.metrics
.http2_connections
.fetch_add(1, Ordering::Relaxed);
}
let extended_connect =
upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
http2_connection(
io,
upgrade_router,
peer,
local,
Transport { tls: false },
None,
extended_connect,
upgrade_state_for_task.clone(),
upgrade_lease,
upgrade_stop,
grace,
http2_settings,
)
.await;
}
Err(error) => {
log_protocol_error(
upgrade_state_for_task.as_ref(),
"h2c_transport_error",
&error,
peer,
local,
"tcp",
"2",
0,
None,
);
tracing::debug!(%error, "h2c upgrade transport failed");
}
}
};
if let Some(state) = upgrade_state {
state.tunnels.spawn(task);
} else {
tokio::spawn(task);
}
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::SWITCHING_PROTOCOLS)
.header(http::header::CONNECTION, "Upgrade")
.header(http::header::UPGRADE, "h2c")
.body(Body::empty())
.expect("static h2c switching response"),
);
}
router.oneshot(request.map(Body::new)).await
}
});
let mut builder = http1::Builder::new();
builder
.timer(TokioTimer::new())
.keep_alive(true)
.max_headers(64)
.max_buf_size(65_536)
.header_read_timeout(Duration::from_secs(10));
let conn = builder
.serve_connection(TokioIo::new(stream), service)
.with_upgrades();
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(error) = result {
log_protocol_error(
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
);
tracing::debug!(%error, "HTTP/1 connection closed");
}
},
_ = stop.cancelled() => {
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
},
}
}
#[derive(Clone, Copy, Debug)]
enum H2ShutdownCause {
Idle,