use std::time::Duration;
use hyper::{Response, StatusCode};
use mini_serve::{body, handler, OnUpgrade, RouteBuilder, ServeError};
use hyper::body::Bytes;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot;
fn echo_upgrade_route(builder: RouteBuilder<()>) -> RouteBuilder<()> {
builder.get(
"/upgrade",
handler(|_req, _state| async {
let mut response = Response::new(body(Bytes::new()));
*response.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
response.extensions_mut().insert(OnUpgrade::new(|mut io| async move {
let mut buf = [0u8; 64];
if let Ok(n) = io.read(&mut buf).await {
let _ = io.write_all(&buf[..n]).await;
let _ = io.flush().await;
}
}));
Ok::<_, ServeError>(response)
}),
)
}
async fn upgrade_handshake(port: u16) -> TcpStream {
let mut stream = TcpStream::connect(format!("127.0.0.1:{port}")).await.unwrap();
stream
.write_all(
b"GET /upgrade HTTP/1.1\r\n\
Host: localhost\r\n\
Connection: upgrade\r\n\
Upgrade: raw\r\n\r\n",
)
.await
.unwrap();
let mut head = Vec::new();
let mut byte = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") {
let n = stream.read(&mut byte).await.unwrap();
assert_ne!(n, 0, "connection closed before the 101");
head.push(byte[0]);
}
let head = String::from_utf8_lossy(&head);
assert!(head.starts_with("HTTP/1.1 101"), "expected a 101, got: {head}");
stream
}
#[tokio::test]
async fn an_upgraded_connection_carries_raw_bytes_both_ways() {
let app = echo_upgrade_route(RouteBuilder::stateless().with_upgrades()).seal();
let port = app.bind_ephemeral().await.unwrap();
let mut stream = upgrade_handshake(port).await;
stream.write_all(b"not http at all").await.unwrap();
let mut buf = [0u8; 64];
let n = tokio::time::timeout(Duration::from_secs(3), stream.read(&mut buf))
.await
.expect("the upgraded stream never answered")
.unwrap();
assert_eq!(&buf[..n], b"not http at all");
}
#[tokio::test]
async fn an_upgrade_without_the_builder_flag_does_not_hand_over() {
let app = echo_upgrade_route(RouteBuilder::stateless()).seal();
let port = app.bind_ephemeral().await.unwrap();
let mut stream = upgrade_handshake(port).await;
stream.write_all(b"anyone there").await.unwrap();
let mut buf = [0u8; 64];
let read = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf)).await;
match read {
Err(_) => {} Ok(Ok(0)) => {} Ok(Ok(n)) => panic!("bytes were echoed without with_upgrades(): {:?}", &buf[..n]),
Ok(Err(_)) => {} }
}
#[tokio::test]
async fn an_upgraded_connection_still_counts_against_max_connections() {
let app = RouteBuilder::stateless()
.with_upgrades()
.with_max_connections(1)
.get(
"/upgrade",
handler(|_req, _state| async {
let mut response = Response::new(body(Bytes::new()));
*response.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
response.extensions_mut().insert(OnUpgrade::new(|mut io| async move {
let mut buf = [0u8; 64];
while let Ok(n) = io.read(&mut buf).await {
if n == 0 {
break;
}
}
}));
Ok::<_, ServeError>(response)
}),
)
.get(
"/plain",
handler(|_req, _state| async {
mini_serve::json(StatusCode::OK, &serde_json::json!({"ok": true}))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let held = upgrade_handshake(port).await;
let mut second = TcpStream::connect(format!("127.0.0.1:{port}")).await.unwrap();
second
.write_all(b"GET /plain HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let mut buf = [0u8; 256];
let blocked = tokio::time::timeout(Duration::from_millis(600), second.read(&mut buf)).await;
assert!(
blocked.is_err(),
"a second connection was served while an upgraded connection held the only permit \
— the upgrade escaped max_connections"
);
drop(held);
let n = tokio::time::timeout(Duration::from_secs(5), second.read(&mut buf))
.await
.expect("the permit was never returned after the upgraded connection closed")
.unwrap();
let response = String::from_utf8_lossy(&buf[..n]);
assert!(
response.starts_with("HTTP/1.1 200"),
"the queued connection was not served after the permit freed: {response}"
);
}
#[tokio::test]
async fn an_upgraded_connection_is_ended_by_the_shutdown_drain() {
const DRAIN: Duration = Duration::from_secs(5);
let app = echo_upgrade_route(RouteBuilder::stateless().with_upgrades()).seal();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let run_task = tokio::spawn(async move {
app.run(listener, async move {
let _ = shutdown_rx.await;
})
.await
});
let _held = upgrade_handshake(port).await;
shutdown_tx.send(()).unwrap();
let stopped = tokio::time::timeout(DRAIN * 4, run_task).await;
assert!(
stopped.is_ok(),
"shutdown never returned — an upgraded connection outlived the drain"
);
}