-
-
Notifications
You must be signed in to change notification settings - Fork 241
Open
Description
Reproducible minimal example:
use salvo::conn::rustls::{Keycert, RustlsConfig};
use salvo::prelude::*;
use std::net::SocketAddr;
pub struct Http3Server {
addr: SocketAddr,
rustls_config: RustlsConfig,
}
impl Http3Server {
pub fn new(addr: SocketAddr, cert: Vec<u8>, key: Vec<u8>) -> Self {
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert).key(key));
Self { addr, rustls_config }
}
pub async fn run(self) {
let router = Router::new()
.push(Router::with_path("/hello").get(hello))
.push(Router::with_path("/").get(index));
let tcp_listener = TcpListener::new(self.addr).rustls(self.rustls_config.clone());
let quinn_listener = QuinnListener::new(
self.rustls_config.build_quinn_config().unwrap(),
self.addr,
);
let acceptor = quinn_listener.join(tcp_listener).bind().await;
Server::new(acceptor).serve(router).await;
}
}
#[handler]
async fn hello() -> &'static str {
"Hello from /hello"
}
#[handler]
async fn index() -> &'static str {
"Welcome to /"
}
#[tokio::main]
async fn main() {
// openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
tracing_subscriber::fmt().init();
let cert = include_bytes!("../cert.pem").to_vec();
let key = include_bytes!("../key.pem").to_vec();
let addr: SocketAddr = "0.0.0.0:4443".parse().unwrap();
let server = Http3Server::new(addr, cert, key);
server.run().await;
}Client to check the stream ended flag:
import asyncio
import ssl
from aioquic.asyncio import connect
from aioquic.asyncio.protocol import QuicConnectionProtocol
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.h3.events import HeadersReceived, DataReceived
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent
class HttpClient(QuicConnectionProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._http = H3Connection(self._quic)
self._stream_ended = asyncio.Event()
def quic_event_received(self, event: QuicEvent):
for http_event in self._http.handle_event(event):
if isinstance(http_event, HeadersReceived):
print("🔹 Headers received:")
for name, value in http_event.headers:
print(f" {name.decode()}: {value.decode()}")
elif isinstance(http_event, DataReceived):
print(f"🔹 Body received: {http_event.data.decode()}")
if http_event.stream_ended:
print("✅ Stream ended.")
self._stream_ended.set()
async def get(self, authority: str, path: str = "/"):
stream_id = self._quic.get_next_available_stream_id()
headers = [
(b":method", b"GET"),
(b":scheme", b"https"),
(b":authority", authority.encode()),
(b":path", path.encode()),
]
self._http.send_headers(stream_id=stream_id, headers=headers, end_stream=True)
self.transmit()
await self._stream_ended.wait()
async def main():
host = "google.com"
# host = "127.0.0.1"
# port = 4443
port = 443
path = "/"
configuration = QuicConfiguration(is_client=True, alpn_protocols=H3_ALPN)
configuration.verify_mode = ssl.CERT_NONE # for testing
async with connect(host, port, configuration=configuration, create_protocol=HttpClient) as client:
await client.get(authority=host, path=path)
if __name__ == "__main__":
asyncio.run(main())If I make a request to google.com using HTTP/3, I see the following:
🔹 Headers received:
:status: 301
location: https://www.google.com/
content-type: text/html; charset=UTF-8
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-paQ1mVBB_NN8msjiAu8Pzw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
date: Fri, 25 Apr 2025 08:58:13 GMT
expires: Sun, 25 May 2025 08:58:13 GMT
cache-control: public, max-age=2592000
server: gws
content-length: 220
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
🔹 Body received: <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>
🔹 Body received:
✅ Stream ended.
If I make a request to the Salvo server provided above:
11:59:09 ❯ python http3.py (http3)
🔹 Headers received:
:status: 200
alt-svc: h3=":4443"; ma=2592000,h3-29=":4443"; ma=2592000
content-type: text/plain; charset=utf-8
🔹 Body received: Welcome to /
It gets stuck and never finishes.
Salvo HTTP/3 server responds with headers and body but the client does not receive the stream end signal (stream_ended is never true). In contrast, requests to google.com succeed and the client detects the stream ending correctly.
I also tried using stream::once with res.stream, but the result was the same. According to the documentation, Salvo does not require manually closing streams.
kudroma404 and cnlancehu
Metadata
Metadata
Assignees
Labels
No labels