pub(crate) mod body;
pub(crate) mod bridge;
pub(crate) mod io;
use super::Backend;
use crate::Version;
use crate::conn::ConnConfig;
use crate::deadline::ConnDeadline;
use crate::header::HeaderVec;
use crate::limits::Limits;
use crate::service::{H1Service, Upgraded};
use crate::write::{self, DateCache, OutBody, ResponseHead};
use bridge::Bridge;
use bytes::Bytes;
use io::{HyperIo, IoShared, SharedIo};
use std::cell::{Cell, RefCell};
use std::io as stdio;
use std::net::SocketAddr;
use std::rc::Rc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
const MIN_HYPER_BUF: usize = 8 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Phase {
Idle,
Head,
Handler,
Write,
}
pub(crate) struct PhaseClock {
phase: Cell<Phase>,
generation: Cell<u64>,
wrote_bytes: Cell<bool>,
body_done: Cell<bool>,
detached: Cell<bool>,
notify: tokio::sync::Notify,
}
impl PhaseClock {
pub(crate) fn new(phase: Phase) -> Self {
Self {
phase: Cell::new(phase),
generation: Cell::new(0),
wrote_bytes: Cell::new(false),
body_done: Cell::new(false),
detached: Cell::new(false),
notify: tokio::sync::Notify::new(),
}
}
pub(crate) fn detach(&self) {
self.detached.set(true);
}
pub(crate) fn phase(&self) -> Phase {
self.phase.get()
}
pub(crate) fn wrote_bytes(&self) -> bool {
self.wrote_bytes.get()
}
pub(crate) fn set(&self, p: Phase) {
if self.detached.get() {
return;
}
if matches!(p, Phase::Idle | Phase::Handler) {
self.wrote_bytes.set(false);
self.body_done.set(false);
}
self.phase.set(p);
self.bump();
}
pub(crate) fn note_write(&self) {
if self.detached.get() {
return;
}
if matches!(self.phase.get(), Phase::Write | Phase::Head) {
self.wrote_bytes.set(true);
}
if self.phase.get() == Phase::Write {
self.bump();
}
}
pub(crate) fn note_body_end(&self) {
self.body_done.set(true);
}
pub(crate) fn note_flush(&self) {
if self.detached.get() {
return;
}
if self.phase.get() == Phase::Write && self.body_done.get() {
self.set(Phase::Idle);
}
}
fn bump(&self) {
self.generation.set(self.generation.get().wrapping_add(1));
self.notify.notify_waiters();
}
}
fn phase_timeout(phase: Phase, limits: &Limits) -> Duration {
match phase {
Phase::Idle => limits.idle_timeout,
Phase::Head => limits.header_timeout,
Phase::Handler => limits.body_timeout,
Phase::Write => limits.write_timeout,
}
}
async fn watchdog(clock: &PhaseClock, limits: &Limits, deadline: &mut ConnDeadline) -> Phase {
loop {
let generation = clock.generation.get();
let phase = clock.phase.get();
deadline.arm(phase_timeout(phase, limits));
tokio::select! {
biased;
() = clock.notify.notified() => continue,
() = deadline.expired() => {
if clock.generation.get() != generation {
continue;
}
return phase;
}
}
}
}
async fn write_error_close<IO: AsyncRead + AsyncWrite + Unpin>(
shared: &Rc<RefCell<IoShared<IO>>>,
date: &Rc<RefCell<DateCache>>,
version: Version,
status: u16,
) {
let mut out = bytes::BytesMut::new();
{
let mut date = date.borrow_mut();
let date_bytes = date.get(std::time::SystemTime::now());
write::write_head(
&mut out,
version,
&ResponseHead {
status,
headers: HeaderVec::new(),
},
&OutBody::None,
date_bytes,
false,
);
}
let mut io = SharedIo(shared.clone());
use tokio::io::AsyncWriteExt;
let _ = io.write_all(&out).await;
let _ = io.flush().await;
}
fn find_io_error(e: &::hyper::Error) -> Option<stdio::Error> {
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(e);
while let Some(s) = source {
if let Some(io_err) = s.downcast_ref::<stdio::Error>() {
return Some(match io_err.raw_os_error() {
Some(code) => stdio::Error::from_raw_os_error(code),
None => stdio::Error::new(io_err.kind(), io_err.to_string()),
});
}
source = s.source();
}
None
}
pub(crate) struct HyperBackend;
impl Backend for HyperBackend {
async fn serve<IO, S>(
io: IO,
service: Rc<S>,
cfg: Rc<ConnConfig>,
date: Rc<RefCell<DateCache>>,
buffered: Bytes,
peer: Option<SocketAddr>,
) -> stdio::Result<Option<Upgraded>>
where
IO: AsyncRead + AsyncWrite + Unpin + 'static,
S: H1Service + 'static,
{
let clock = Rc::new(PhaseClock::new(Phase::Idle));
let (hyper_io, shared) = HyperIo::new(io, buffered, clock.clone());
let upgrade_slot = Rc::new(RefCell::new(None));
let sent_101 = Rc::new(Cell::new(false));
let req_version = Rc::new(Cell::new(Version::Http11));
let bridge = Bridge {
service,
cfg: cfg.clone(),
upgrade_slot: upgrade_slot.clone(),
sent_101: sent_101.clone(),
phase: clock.clone(),
req_version: req_version.clone(),
peer,
};
let mut builder = ::hyper::server::conn::http1::Builder::new();
builder
.header_read_timeout(None)
.max_buf_size(cfg.limits.max_head_bytes.max(MIN_HYPER_BUF))
.max_headers(cfg.limits.max_headers)
.half_close(false)
.pipeline_flush(false);
let mut conn = builder.serve_connection(hyper_io, bridge);
let mut deadline = ConnDeadline::new(cfg.tick);
let result = {
let fut = std::future::poll_fn(|cx| conn.poll_without_shutdown(cx));
tokio::pin!(fut);
tokio::select! {
biased;
expired = watchdog(&clock, &cfg.limits, &mut deadline) => Err(expired),
r = &mut fut => Ok(r),
}
};
match result {
Err(Phase::Idle | Phase::Write) => {
drop(conn);
Ok(None)
}
Err(phase @ (Phase::Head | Phase::Handler)) => {
drop(conn);
let version = match phase {
Phase::Handler => req_version.get(),
_ => Version::Http11,
};
debug_assert!(
phase != Phase::Handler || !clock.wrote_bytes(),
"no final-response bytes can be on the wire in Phase::Handler; \
a new transition into it, or latching interim writes, would \
silently start suppressing the body-timeout 408",
);
if !clock.wrote_bytes() {
deadline.arm(cfg.limits.write_timeout);
tokio::select! {
biased;
() = deadline.expired() => {}
() = write_error_close(&shared, &date, version, 408) => {}
}
}
Ok(None)
}
Ok(Ok(())) => {
if sent_101.get() && upgrade_slot.borrow_mut().take().is_some() {
let parts = conn.into_parts();
clock.detach();
return Ok(Some(Upgraded {
peer,
buffered: parts.read_buf,
io: Box::new(SharedIo(parts.io.0)),
}));
}
Ok(None)
}
Ok(Err(e)) => {
if let Some(io_err) = find_io_error(&e) {
Err(io_err)
} else {
Ok(None)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::write::DateCache;
use crate::{ConnConfig, Limits, Request, Response};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn hello(_req: Request) -> Response {
Response::text("hi")
}
async fn echo(mut req: Request) -> Response {
match req.body.collect(1024 * 1024).await {
Ok(b) => Response::ok().with_body(crate::ResponseBody::Full(b)),
Err(e) => Response::status_only(e.status()),
}
}
fn quick(mut limits: Limits) -> Limits {
limits.idle_timeout = Duration::from_millis(200);
limits.header_timeout = Duration::from_millis(200);
limits
}
fn cfg(limits: Limits) -> Rc<ConnConfig> {
Rc::new(ConnConfig {
limits: quick(limits),
tick: Duration::from_millis(10),
server_name: None,
})
}
struct TickStream {
remaining: usize,
gap: Duration,
sleep: std::pin::Pin<Box<tokio::time::Sleep>>,
chunk: Bytes,
}
impl TickStream {
fn body(count: usize, gap: Duration, chunk: Bytes) -> crate::ResponseBody {
crate::ResponseBody::Stream(Box::pin(TickStream {
remaining: count,
gap,
sleep: Box::pin(tokio::time::sleep(gap)),
chunk,
}))
}
}
impl crate::service::futures_stream::Stream for TickStream {
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<Bytes, crate::BodyError>>> {
use std::future::Future;
use std::task::Poll;
let this = self.get_mut();
if this.remaining == 0 {
return Poll::Ready(None);
}
if this.gap > Duration::ZERO {
match this.sleep.as_mut().poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(()) => {
this.sleep
.as_mut()
.reset(tokio::time::Instant::now() + this.gap);
}
}
}
this.remaining -= 1;
Poll::Ready(Some(Ok(this.chunk.clone())))
}
}
async fn exchange<S>(input: &'static [u8], service: S, limits: Limits) -> (String, bool)
where
S: crate::H1Service + 'static,
{
exchange_with_cfg(input, service, cfg(limits)).await
}
async fn exchange_with_cfg<S>(
input: &'static [u8],
service: S,
config: Rc<ConnConfig>,
) -> (String, bool)
where
S: crate::H1Service + 'static,
{
let (mut client, server) = tokio::io::duplex(64 * 1024);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(service),
config,
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
local
.run_until(async move {
client.write_all(input).await.unwrap();
let mut out = Vec::new();
let closed = matches!(
tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut out))
.await,
Ok(Ok(_))
);
let _ = tokio::time::timeout(Duration::from_secs(2), task).await;
(String::from_utf8_lossy(&out).into_owned(), closed)
})
.await
}
#[tokio::test]
async fn serves_a_single_request() {
let (out, _closed) = exchange(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n",
hello,
Limits::default(),
)
.await;
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
assert!(out.ends_with("hi"), "{out}");
}
#[tokio::test]
async fn full_body_uses_content_length_framing() {
let (out, _closed) = exchange(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n",
hello,
Limits::default(),
)
.await;
assert!(
out.to_ascii_lowercase().contains("content-length: 2"),
"{out}"
);
assert!(
!out.to_ascii_lowercase().contains("transfer-encoding"),
"{out}"
);
}
#[tokio::test]
async fn echoes_content_length_and_chunked_bodies() {
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello",
echo,
Limits::default(),
)
.await;
assert!(out.ends_with("hello"), "{out}");
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nhello\r\n0\r\n\r\n",
echo,
Limits::default(),
)
.await;
assert!(out.ends_with("hello"), "{out}");
}
#[tokio::test]
async fn a_quiet_keep_alive_connection_closes_on_the_idle_deadline() {
let started = tokio::time::Instant::now();
let (out, _closed) = exchange(
b"GET / HTTP/1.1\r\nHost: a\r\n\r\n",
hello,
Limits::default(),
)
.await;
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
assert!(
started.elapsed() < Duration::from_millis(1500),
"closed after {:?}, so the connection never left Phase::Write",
started.elapsed()
);
}
#[tokio::test]
async fn idle_timeout_closes_silently() {
let (out, closed) = exchange(b"", hello, Limits::default()).await;
assert!(out.is_empty(), "no response owed on idle close: {out}");
assert!(
closed,
"the idle deadline must actually close the connection"
);
}
#[tokio::test]
async fn header_timeout_writes_408() {
let (out, _closed) = exchange(b"GET / HTT", hello, Limits::default()).await;
assert!(out.starts_with("HTTP/1.1 408"), "{out}");
}
#[tokio::test]
async fn the_408_write_is_bounded_when_the_peer_never_reads() {
let limits = Limits {
write_timeout: Duration::from_millis(100),
..Default::default()
};
let (mut client, server) = tokio::io::duplex(8);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(hello),
cfg(limits),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let finished = local
.run_until(async move {
client.write_all(b"GET / HTT").await.unwrap();
let r = tokio::time::timeout(Duration::from_secs(2), task).await;
drop(client);
r
})
.await;
assert!(
finished.is_ok(),
"an un-deadlined 408 write pins the connection forever"
);
}
#[tokio::test]
async fn header_timeout_writes_408_on_a_reused_connection() {
let (mut client, server) = tokio::io::duplex(64 * 1024);
let config = Rc::new(ConnConfig {
limits: Limits {
idle_timeout: Duration::from_secs(1),
header_timeout: Duration::from_millis(200),
..Default::default()
},
tick: Duration::from_millis(10),
server_name: None,
});
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(hello),
config,
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let (first, second) = local
.run_until(async move {
client
.write_all(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n")
.await
.unwrap();
let mut first = String::new();
let mut buf = [0u8; 256];
while !first.ends_with("hi") {
let n = client.read(&mut buf).await.unwrap();
assert!(n > 0, "server closed early: {first}");
first.push_str(&String::from_utf8_lossy(&buf[..n]));
}
client.write_all(b"GET / HTT").await.unwrap();
let mut rest = Vec::new();
let _ = tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut rest))
.await;
let _ = task.await;
(first, String::from_utf8_lossy(&rest).into_owned())
})
.await;
assert!(first.starts_with("HTTP/1.1 200 OK"), "{first}");
assert!(
second.starts_with("HTTP/1.1 408"),
"the second request is owed a 408 too, cleanly after the first \
response rather than spliced into it: {second:?}"
);
}
#[tokio::test]
async fn body_timeout_writes_408() {
let limits = Limits {
body_timeout: Duration::from_millis(100),
..Default::default()
};
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\nhel",
echo,
limits,
)
.await;
assert!(out.starts_with("HTTP/1.1 408"), "{out}");
}
#[tokio::test]
async fn an_interim_100_continue_does_not_suppress_the_body_timeout_408() {
let limits = Limits {
body_timeout: Duration::from_millis(100),
..Default::default()
};
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n",
echo,
limits,
)
.await;
assert!(
out.starts_with("HTTP/1.1 100 Continue"),
"hyper answers the Expect eagerly: {out:?}"
);
assert!(
out.contains("HTTP/1.1 408"),
"the interim response must not suppress the 408: {out:?}"
);
}
#[tokio::test]
async fn a_head_response_returns_the_connection_to_the_idle_deadline() {
async fn head_hello(_req: Request) -> Response {
Response::text("hi")
}
let started = tokio::time::Instant::now();
let (out, _closed) = exchange(
b"HEAD / HTTP/1.1\r\nHost: a\r\n\r\n",
head_hello,
Limits::default(),
)
.await;
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out:?}");
assert!(
!out.ends_with("hi"),
"a HEAD response carries no body: {out:?}"
);
assert!(
started.elapsed() < Duration::from_millis(1500),
"closed after {:?}, so the connection never left Phase::Write",
started.elapsed()
);
}
#[tokio::test]
async fn a_head_response_keeps_the_connection_reusable() {
let (out, _closed) = exchange(
b"HEAD / HTTP/1.1\r\nHost: a\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n",
hello,
Limits::default(),
)
.await;
assert_eq!(
out.matches("HTTP/1.1 200 OK").count(),
2,
"the follow-up request must be served too: {out:?}"
);
assert!(out.ends_with("hi"), "only the GET carries a body: {out:?}");
}
#[tokio::test]
async fn response_echoes_the_request_version() {
let (out, _closed) = exchange(
b"GET / HTTP/1.0\r\nHost: a\r\n\r\n",
hello,
Limits::default(),
)
.await;
assert!(out.starts_with("HTTP/1.0 200 OK"), "{out}");
}
#[tokio::test]
async fn a_progressing_stream_outlives_every_deadline() {
async fn stream(_req: Request) -> Response {
Response::ok().with_body(TickStream::body(
10,
Duration::from_millis(30),
Bytes::from_static(b"chunk"),
))
}
let limits = Limits {
write_timeout: Duration::from_millis(300),
..Default::default()
};
let (out, _closed) = exchange(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n",
stream,
limits,
)
.await;
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
assert_eq!(out.matches("5\r\nchunk\r\n").count(), 10, "{out}");
}
#[tokio::test]
async fn a_stalled_write_closes_silently_without_splicing_a_408() {
async fn stream(_req: Request) -> Response {
Response::ok().with_body(TickStream::body(
64,
Duration::ZERO,
Bytes::from(vec![b'x'; 4096]),
))
}
let limits = Limits {
write_timeout: Duration::from_millis(100),
..Default::default()
};
let (mut client, server) = tokio::io::duplex(512);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(stream),
cfg(limits),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let (out, closed) = local
.run_until(async move {
client
.write_all(
b"GET / HTTP/1.1\r\nHost: a\r\n\r\nGET /two HTTP/1.1\r\nHost: a\r\n\r\n",
)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(400)).await;
let mut out = Vec::new();
let closed = matches!(
tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut out))
.await,
Ok(Ok(_))
);
let _ = tokio::time::timeout(Duration::from_secs(2), task).await;
(String::from_utf8_lossy(&out).into_owned(), closed)
})
.await;
assert!(out.starts_with("HTTP/1.1 200 OK"), "{out}");
assert!(
!out.contains("408"),
"a 408 must never be spliced into a response in flight: {out}"
);
assert!(out.len() < 4096, "the stall must cut the body short: {out}");
assert!(closed, "the stalled write must close the connection");
}
#[tokio::test]
async fn a_partial_head_arriving_mid_write_is_bounded_by_the_idle_deadline() {
async fn stream(_req: Request) -> Response {
Response::ok().with_body(TickStream::body(
10,
Duration::from_millis(60),
Bytes::from_static(b"chunk"),
))
}
let (mut client, server) = tokio::io::duplex(64 * 1024);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(stream),
cfg(Limits::default()),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let (first, rest, closed) = local
.run_until(async move {
client
.write_all(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n")
.await
.unwrap();
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
assert!(n > 0, "no response bytes arrived");
client.write_all(b"GET /two HTT").await.unwrap();
let mut first = String::from_utf8_lossy(&buf[..n]).into_owned();
while !first.ends_with("0\r\n\r\n") {
let n = client.read(&mut buf).await.unwrap();
assert!(n > 0, "server closed before the response finished: {first}");
first.push_str(&String::from_utf8_lossy(&buf[..n]));
}
let mut rest = Vec::new();
let closed = matches!(
tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut rest))
.await,
Ok(Ok(_))
);
let _ = tokio::time::timeout(Duration::from_secs(2), task).await;
(first, String::from_utf8_lossy(&rest).into_owned(), closed)
})
.await;
assert!(first.starts_with("HTTP/1.1 200 OK"), "{first}");
assert!(
rest.is_empty(),
"a partial head buffered during Phase::Write must not produce a \
408 once the connection returns to idle: {rest:?}"
);
assert!(
closed,
"the buffered partial head must still be bounded by idle_timeout"
);
}
#[tokio::test]
async fn upgrade_hands_back_transport_and_buffered_bytes() {
async fn switching(_req: Request) -> Response {
Response::new(101)
.header(crate::HeaderId::Upgrade, Bytes::from_static(b"raw"))
.header(crate::HeaderId::Connection, Bytes::from_static(b"upgrade"))
}
let (mut client, server) = tokio::io::duplex(4096);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(switching),
cfg(Limits::default()),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let upgraded = local
.run_until(async move {
client
.write_all(b"GET / HTTP/1.1\r\nHost: a\r\nConnection: upgrade\r\nUpgrade: raw\r\n\r\nFIRSTFRAME")
.await
.unwrap();
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
let head = String::from_utf8_lossy(&buf[..n]).into_owned();
assert!(head.starts_with("HTTP/1.1 101"), "{head}");
task.await.expect("join").expect("serve")
})
.await;
let upgraded = upgraded.expect("transport handed back");
assert_eq!(&upgraded.buffered[..], b"FIRSTFRAME");
}
#[tokio::test]
async fn unread_body_forces_close() {
async fn ignore(_req: Request) -> Response {
Response::status_only(404)
}
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\nhelloGET / HTTP/1.1\r\nHost: a\r\n\r\n",
ignore,
Limits::default(),
)
.await;
assert_eq!(
out.matches("HTTP/1.1").count(),
1,
"unread body must not enable reuse: {out}"
);
}
#[tokio::test]
async fn a_handler_connection_header_cannot_defeat_the_unread_body_close() {
async fn ignore(_req: Request) -> Response {
Response::status_only(404).header(
crate::HeaderId::Connection,
Bytes::from_static(b"keep-alive"),
)
}
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\nhelloGET / HTTP/1.1\r\nHost: a\r\n\r\n",
ignore,
Limits::default(),
)
.await;
assert_eq!(
out.matches("HTTP/1.1").count(),
1,
"unread body must not enable reuse: {out}"
);
assert!(
out.to_ascii_lowercase().contains("connection: close"),
"{out}"
);
}
#[tokio::test]
async fn an_invalid_handler_connection_header_does_not_suppress_ours() {
async fn bad(_req: Request) -> Response {
Response::status_only(200).header(
crate::HeaderId::Connection,
Bytes::from_static(b"keep\r\nalive"),
)
}
let (out, _closed) = exchange(
b"GET / HTTP/1.0\r\nHost: a\r\nConnection: keep-alive\r\n\r\n",
bad,
Limits::default(),
)
.await;
assert!(
out.to_ascii_lowercase().contains("connection: keep-alive"),
"an unwritable handler field must not leave the response without \
one: {out}"
);
}
#[tokio::test]
async fn an_invalid_handler_server_header_does_not_suppress_the_configured_one() {
async fn bad(_req: Request) -> Response {
Response::status_only(200)
.header(crate::HeaderId::Server, Bytes::from_static(b"ba\r\nd"))
}
let config = Rc::new(ConnConfig {
limits: quick(Limits::default()),
tick: Duration::from_millis(10),
server_name: Some(Bytes::from_static(b"armature")),
});
let (out, _closed) = exchange_with_cfg(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n",
bad,
config,
)
.await;
assert!(
out.to_ascii_lowercase().contains("server: armature"),
"{out}"
);
assert!(!out.contains("ba\r\nd"), "{out}");
}
#[tokio::test]
async fn an_unread_content_length_zero_body_still_allows_reuse() {
async fn ignore(_req: Request) -> Response {
Response::status_only(204)
}
let (out, _closed) = exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 0\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\n\r\n",
ignore,
Limits::default(),
)
.await;
assert_eq!(
out.matches("HTTP/1.1 204").count(),
2,
"an already-exhausted body must not force a close: {out}"
);
assert!(
!out.to_ascii_lowercase().contains("connection: close"),
"{out}"
);
}
#[tokio::test]
async fn the_body_timeout_408_echoes_an_http_10_request_version() {
let limits = Limits {
body_timeout: Duration::from_millis(100),
..Default::default()
};
let (out, _closed) = exchange(
b"POST / HTTP/1.0\r\nHost: a\r\nContent-Length: 5\r\n\r\nhel",
echo,
limits,
)
.await;
assert!(out.starts_with("HTTP/1.0 408"), "{out}");
}
#[tokio::test]
async fn a_retained_body_across_an_upgrade_neither_panics_nor_hangs() {
thread_local! {
static LEAKED: RefCell<Option<crate::Body>> = const { RefCell::new(None) };
}
async fn switching(req: Request) -> Response {
LEAKED.with(|slot| *slot.borrow_mut() = Some(req.body));
Response::new(101)
.header(crate::HeaderId::Upgrade, Bytes::from_static(b"raw"))
.header(crate::HeaderId::Connection, Bytes::from_static(b"upgrade"))
}
let (mut client, server) = tokio::io::duplex(4096);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(switching),
cfg(Limits::default()),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
let served = local
.run_until(async move {
client
.write_all(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: upgrade\r\nUpgrade: raw\r\n\r\n",
)
.await
.unwrap();
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
let head = String::from_utf8_lossy(&buf[..n]).into_owned();
assert!(head.starts_with("HTTP/1.1 101"), "{head}");
tokio::time::timeout(Duration::from_secs(2), task).await
})
.await;
let served = served
.expect("the serve future must resolve, not hang")
.expect("the worker task must not panic")
.expect("serve");
assert!(
served.is_some(),
"hyper's request body does not borrow the transport, so the \
handoff still happens — the divergence from native's Ok(None)"
);
LEAKED.with(|slot| slot.borrow_mut().take());
}
async fn upgrade_exchange<S>(input: &'static [u8], service: S) -> Option<Upgraded>
where
S: crate::H1Service + 'static,
{
let (mut client, server) = tokio::io::duplex(4096);
let local = tokio::task::LocalSet::new();
let task = local.spawn_local(HyperBackend::serve(
server,
Rc::new(service),
cfg(Limits::default()),
Rc::new(RefCell::new(DateCache::new())),
Bytes::new(),
None,
));
local
.run_until(async move {
client.write_all(input).await.unwrap();
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
let head = String::from_utf8_lossy(&buf[..n]).into_owned();
assert!(head.starts_with("HTTP/1.1 101"), "{head}");
tokio::time::timeout(Duration::from_secs(2), task)
.await
.expect("the serve future must resolve, not hang")
.expect("the worker task must not panic")
.expect("serve")
})
.await
}
async fn switching(_req: Request) -> Response {
Response::new(101)
.header(crate::HeaderId::Upgrade, Bytes::from_static(b"raw"))
.header(crate::HeaderId::Connection, Bytes::from_static(b"upgrade"))
}
#[tokio::test]
async fn an_unread_body_forfeits_the_upgrade_handoff() {
let served = upgrade_exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nConnection: upgrade\r\nUpgrade: raw\r\n\
Content-Length: 5\r\n\r\nhello",
switching,
)
.await;
assert!(
served.is_none(),
"an unread request body forfeits the handoff, as it does natively"
);
}
#[tokio::test]
async fn a_body_read_to_its_end_still_upgrades() {
async fn drain_then_switch(mut req: Request) -> Response {
req.body.collect(1024).await.expect("body");
switching(req).await
}
let served = upgrade_exchange(
b"POST / HTTP/1.1\r\nHost: a\r\nConnection: upgrade\r\nUpgrade: raw\r\n\
Content-Length: 5\r\n\r\nhello",
drain_then_switch,
)
.await;
assert!(
served.is_some(),
"a body read to its end leaves the handoff intact"
);
}
mod phase_clock {
use super::super::{Phase, PhaseClock};
#[test]
fn note_write_latches_only_in_the_write_and_head_phases() {
for (phase, expected) in [
(Phase::Idle, false),
(Phase::Head, true),
(Phase::Handler, false),
(Phase::Write, true),
] {
let c = PhaseClock::new(phase);
assert!(!c.wrote_bytes(), "{phase:?} starts clean");
c.note_write();
assert_eq!(
c.wrote_bytes(),
expected,
"note_write in {phase:?} should {} latch",
if expected { "" } else { "not" }
);
}
}
#[test]
fn only_a_write_phase_write_re_arms_the_deadline() {
for (phase, expected) in [
(Phase::Idle, false),
(Phase::Head, false),
(Phase::Handler, false),
(Phase::Write, true),
] {
let c = PhaseClock::new(phase);
let before = c.generation.get();
c.note_write();
assert_eq!(
c.generation.get() > before,
expected,
"note_write in {phase:?} bumped the generation unexpectedly"
);
}
}
#[test]
fn entering_idle_or_handler_clears_the_per_response_flags() {
for reset in [Phase::Idle, Phase::Handler] {
let c = PhaseClock::new(Phase::Write);
c.note_write();
c.note_body_end();
assert!(c.wrote_bytes());
c.set(reset);
assert!(!c.wrote_bytes(), "{reset:?} must clear wrote_bytes");
c.set(Phase::Write);
c.note_flush();
assert_eq!(
c.phase(),
Phase::Write,
"{reset:?} must clear body_done, so a flush alone cannot \
end the response"
);
}
}
#[test]
fn entering_head_or_write_preserves_the_per_response_flags() {
for keep in [Phase::Head, Phase::Write] {
let c = PhaseClock::new(Phase::Write);
c.note_write();
c.set(keep);
assert!(c.wrote_bytes(), "{keep:?} must not clear wrote_bytes");
}
}
#[test]
fn note_flush_returns_to_idle_only_on_a_finished_write() {
let c = PhaseClock::new(Phase::Write);
c.note_flush();
assert_eq!(c.phase(), Phase::Write);
let c = PhaseClock::new(Phase::Write);
c.note_body_end();
c.note_flush();
assert_eq!(c.phase(), Phase::Idle);
for phase in [Phase::Idle, Phase::Head, Phase::Handler] {
let c = PhaseClock::new(phase);
c.note_body_end();
c.note_flush();
assert_eq!(c.phase(), phase, "note_flush must be inert in {phase:?}");
}
}
#[test]
fn every_set_bumps_the_generation() {
let c = PhaseClock::new(Phase::Idle);
let mut last = c.generation.get();
for phase in [
Phase::Head,
Phase::Handler,
Phase::Write,
Phase::Idle,
Phase::Idle,
] {
c.set(phase);
assert!(c.generation.get() > last, "set({phase:?}) must bump");
last = c.generation.get();
}
}
#[test]
fn a_detached_clock_ignores_everything() {
let c = PhaseClock::new(Phase::Write);
c.note_body_end();
c.detach();
let generation = c.generation.get();
c.note_write();
c.note_flush();
c.set(Phase::Idle);
assert_eq!(c.phase(), Phase::Write);
assert!(!c.wrote_bytes());
assert_eq!(c.generation.get(), generation, "a detached clock is quiet");
}
}
}