Version
h3-webtransport 0.1.2
Platform
Linux desktop 7.1.7 NixOS x86_64 GNU/Linux
Summary
accept_bi() head-of-line blocks while resolving the first frame
Code Sample
Client-side pseudocode, using a low-level HTTP/3/WebTransport peer so the first stream header can be withheld deliberately:
let session = establish_webtransport_session().await?;
// Stream A is accepted by QUIC, but never becomes classifiable by h3.
let (_send_a, _recv_a) = session.open_raw_bi().await?;
// Stream B is valid and complete.
let (mut send_b, _recv_b) = session.open_raw_bi().await?;
send_b.write_all(&encode_webtransport_bidi_header(session.id())).await?;
send_b.write_all(b"request").await?;
send_b.finish()?;
Server:
let first = session.accept_bi();
tokio::pin!(first);
assert!(tokio::time::timeout(Duration::from_millis(250), &mut first)
.await
.is_err());
// This remains blocked even though stream B is complete, because the only
// accept_bi future is still resolving stream A's first frame.
The relevant implementation sequence is effectively:
let stream = poll_accept_request_stream().await?;
let mut resolver = create_resolver(FrameStream::new(stream));
let frame = poll_fn(|cx| resolver.frame_stream.poll_next(cx)).await;
Expected Behavior
Accepting later streams should remain possible while each newly accepted stream independently resolves its first frame. An incomplete stream should consume only its own stream and timeout budget, not the connection's global acceptance path.
Actual Behavior
WebTransportSession::accept_bi() first accepts one QUIC bidirectional stream and then synchronously waits for that stream's first protocol frame. It does not return control to the caller until the first stream is classified as a WebTransport stream or an ordinary HTTP/3 request.
A peer can therefore open stream A and send no first-frame bytes, then open a complete stream B. The server remains blocked resolving stream A and never accepts stream B. A single incomplete stream stalls all later bidirectional streams on that HTTP/3/WebTransport connection.
Suggested upstream fix
Separate QUIC stream acceptance from first-frame resolution. Return an unresolved accepted stream, or maintain a bounded set of per-stream resolver futures and yield whichever resolves first. The API should also define a stream-local cancellation path that does not drop or close the whole h3 connection.
Version
h3-webtransport 0.1.2
Platform
Linux desktop 7.1.7 NixOS x86_64 GNU/Linux
Summary
accept_bi()head-of-line blocks while resolving the first frameCode Sample
Client-side pseudocode, using a low-level HTTP/3/WebTransport peer so the first stream header can be withheld deliberately:
Server:
The relevant implementation sequence is effectively:
Expected Behavior
Accepting later streams should remain possible while each newly accepted stream independently resolves its first frame. An incomplete stream should consume only its own stream and timeout budget, not the connection's global acceptance path.
Actual Behavior
WebTransportSession::accept_bi()first accepts one QUIC bidirectional stream and then synchronously waits for that stream's first protocol frame. It does not return control to the caller until the first stream is classified as a WebTransport stream or an ordinary HTTP/3 request.A peer can therefore open stream A and send no first-frame bytes, then open a complete stream B. The server remains blocked resolving stream A and never accepts stream B. A single incomplete stream stalls all later bidirectional streams on that HTTP/3/WebTransport connection.
Suggested upstream fix
Separate QUIC stream acceptance from first-frame resolution. Return an unresolved accepted stream, or maintain a bounded set of per-stream resolver futures and yield whichever resolves first. The API should also define a stream-local cancellation path that does not drop or close the whole h3 connection.