Skip to content

accept_bi() head-of-line blocks while resolving the first frame #348

Description

@spotdemo4

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions