hyperium / hyperium/h3

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

Open
#348 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-h3-webtransport
Dominant language
Rust
Stars
890
Forks
136
Avg merge
14d 20h
Merged PRs (30d)
2

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at WebTransportSession::accept_bi() and trace the sequence from poll_accept_request_stream() through the resolver's first-frame wait. Reproduce the case with the provided streams, then determine how later streams can progress without letting an incomplete stream stall the connection. Done means stream resolution is independent or fairly scheduled, with a stream-local cancellation path that does not close the h3 connection.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
networking
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.