Skip to content

Commit 3c7492d

Browse files
authored
Merge pull request #143 from englishm-cloudflare/me/web-transport-upgrade
2 parents 9fef91c + f1e7fff commit 3c7492d

9 files changed

Lines changed: 233 additions & 143 deletions

File tree

Cargo.lock

Lines changed: 141 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ members = [
1313
resolver = "2"
1414

1515
[workspace.dependencies]
16-
web-transport = "0.3"
16+
web-transport = "0.10"
1717
tracing = "0.1"
1818
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
1919

moq-native-ietf/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ categories = ["multimedia", "network-programming", "web-programming"]
1414
[dependencies]
1515
moq-transport = { path = "../moq-transport", version = "0.12" }
1616
web-transport = { workspace = true }
17-
web-transport-quinn = "0.3"
17+
web-transport-quinn = { version = "0.11", default-features = false, features = ["ring"] }
1818

1919
rustls = { version = "0.23", features = ["ring"] }
2020
rustls-pemfile = "2"

moq-native-ietf/src/quic.rs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ impl Endpoint {
160160

161161
if let Some(mut config) = config.tls.server {
162162
config.alpn_protocols = vec![
163-
web_transport_quinn::ALPN.to_vec(),
163+
web_transport_quinn::ALPN.as_bytes().to_vec(),
164164
moq_transport::setup::ALPN.to_vec(),
165165
];
166166
config.key_log = Arc::new(rustls::KeyLogFile::new());
@@ -305,22 +305,28 @@ impl Server {
305305
server_name,
306306
);
307307

308-
let session = match alpn.as_bytes() {
309-
web_transport_quinn::ALPN => {
310-
// Wait for the CONNECT request.
311-
let request = web_transport_quinn::accept(conn)
312-
.await
313-
.context("failed to receive WebTransport request")?;
314-
315-
// Accept the CONNECT request.
316-
request
317-
.ok()
318-
.await
319-
.context("failed to respond to WebTransport request")?
320-
}
321-
// A bit of a hack to pretend like we're a WebTransport session
322-
moq_transport::setup::ALPN => conn.into(),
323-
_ => anyhow::bail!("unsupported ALPN: {}", alpn),
308+
let alpn_bytes = alpn.as_bytes();
309+
let session = if alpn_bytes == web_transport_quinn::ALPN.as_bytes() {
310+
// Wait for the WebTransport CONNECT request (includes H3 SETTINGS exchange).
311+
let request = web_transport_quinn::Request::accept(conn)
312+
.await
313+
.context("failed to receive WebTransport request")?;
314+
315+
// Accept the CONNECT request.
316+
request
317+
.ok()
318+
.await
319+
.context("failed to respond to WebTransport request")?
320+
} else if alpn_bytes == moq_transport::setup::ALPN {
321+
// Raw QUIC mode — create a "fake" WebTransport session with no H3 framing.
322+
let request = url::Url::parse("moqt://localhost").unwrap();
323+
web_transport_quinn::Session::raw(
324+
conn,
325+
request,
326+
web_transport_quinn::proto::ConnectResponse::default(),
327+
)
328+
} else {
329+
anyhow::bail!("unsupported ALPN: {}", alpn)
324330
};
325331

326332
Ok((session.into(), connection_id_hex))
@@ -373,7 +379,7 @@ impl Client {
373379

374380
// TODO support connecting to both ALPNs at the same time
375381
config.alpn_protocols = vec![match url.scheme() {
376-
"https" => web_transport_quinn::ALPN.to_vec(),
382+
"https" => web_transport_quinn::ALPN.as_bytes().to_vec(),
377383
"moqt" => moq_transport::setup::ALPN.to_vec(),
378384
_ => anyhow::bail!("url scheme must be 'https' or 'moqt'"),
379385
}];
@@ -426,8 +432,12 @@ impl Client {
426432
.to_string();
427433

428434
let session = match url.scheme() {
429-
"https" => web_transport_quinn::connect_with(connection, url).await?,
430-
"moqt" => connection.into(),
435+
"https" => web_transport_quinn::Session::connect(connection, url.clone()).await?,
436+
"moqt" => web_transport_quinn::Session::raw(
437+
connection,
438+
url.clone(),
439+
web_transport_quinn::proto::ConnectResponse::default(),
440+
),
431441
_ => unreachable!(),
432442
};
433443

moq-test-client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ path = "src/main.rs"
1818
[dependencies]
1919
moq-transport = { path = "../moq-transport", version = "0.12" }
2020
moq-native-ietf = { path = "../moq-native-ietf", version = "0.7" }
21-
web-transport = "0.3"
21+
web-transport = { workspace = true }
2222

2323
url = "2"
2424

moq-transport/Cargo.toml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,6 @@ uuid = { version = "1", features = ["v4"] }
2323

2424
web-transport = { workspace = true }
2525

26-
# Used for is_graceful_close() to structurally match on connection errors
27-
# and decode WebTransport error codes. This couples moq-transport to the
28-
# quinn-based WebTransport implementation. When transitioning to a different
29-
# backend (e.g., tokio-quiche), these dependencies should be replaced with
30-
# equivalent types from the new implementation.
31-
web-transport-quinn = "0.3"
32-
web-transport-proto = "0.2"
33-
quinn = "0.11"
34-
3526
paste = "1"
3627
futures = "0.3"
3728
serde = { version = "1", features = ["derive"] }

moq-transport/src/session/error.rs

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@ use crate::{coding, serve, setup};
22

33
#[derive(thiserror::Error, Debug, Clone)]
44
pub enum SessionError {
5-
#[error("webtransport session: {0}")]
6-
Session(#[from] web_transport::SessionError),
7-
8-
#[error("webtransport write: {0}")]
9-
Write(#[from] web_transport::WriteError),
10-
11-
#[error("webtransport read: {0}")]
12-
Read(#[from] web_transport::ReadError),
5+
#[error("webtransport error: {0}")]
6+
WebTransport(#[from] web_transport::Error),
137

148
#[error("encode error: {0}")]
159
Encode(#[from] coding::EncodeError),
@@ -53,9 +47,7 @@ impl SessionError {
5347
// PROTOCOL_VIOLATION (0x3) - The role negotiated in the handshake was violated
5448
Self::RoleViolation => 0x3,
5549
// INTERNAL_ERROR (0x1) - Generic internal errors
56-
Self::Session(_) => 0x1,
57-
Self::Read(_) => 0x1,
58-
Self::Write(_) => 0x1,
50+
Self::WebTransport(_) => 0x1,
5951
Self::Encode(_) => 0x1,
6052
Self::BoundsExceeded(_) => 0x1,
6153
Self::Internal => 0x1,
@@ -79,42 +71,48 @@ impl SessionError {
7971

8072
/// Returns true if this error represents a graceful connection close.
8173
///
82-
/// A graceful close occurs when the peer sends APPLICATION_CLOSE with error code 0
83-
/// (NO_ERROR). This is normal session termination, not an error condition.
74+
/// For WebTransport, a graceful close is a `CLOSE_WEBTRANSPORT_SESSION` capsule
75+
/// with code 0. For raw QUIC, it's `APPLICATION_CLOSE` with code 0 (NO_ERROR).
76+
/// Both are normal session termination, not error conditions.
8477
///
8578
/// This method checks for:
86-
/// - WebTransport close with code 0 (HTTP/3 encoded as 0x52e4a40fa8db)
79+
/// - WebTransport `Closed(0, _)` — web-transport-quinn v0.11+ typically converts
80+
/// HTTP/3-encoded `ApplicationClosed` codes into `WebTransportError::Closed(code, reason)`
81+
/// during `SessionError` conversion when decoding via `error_from_http3` succeeds
8782
/// - Raw QUIC `ApplicationClosed` with code 0
8883
/// - The local side closing the connection (`LocallyClosed`)
8984
///
9085
/// ## Implementation Notes
9186
///
92-
/// We pattern match on `web_transport_quinn::SessionError` variants to access the
93-
/// underlying `quinn::ConnectionError`. For WebTransport connections, the close code
94-
/// is encoded using HTTP/3 error code space, which we decode using
95-
/// `web_transport_proto::error_from_http3()`.
87+
/// We pattern match on `web_transport_quinn::SessionError` variants. In v0.11+,
88+
/// WebTransport graceful closes arrive as `WebTransportError::Closed(0, _)` because
89+
/// the crate decodes HTTP/3 error codes at the `SessionError` level. For raw QUIC
90+
/// connections, the close code is checked directly on `ConnectionError::ApplicationClosed`.
9691
///
9792
/// **Coupling note**: This implementation is coupled to `web-transport-quinn` and
9893
/// `quinn`. When transitioning to a different WebTransport backend (e.g., tokio-quiche),
9994
/// ensure the replacement provides equivalent error introspection, or update this
10095
/// method to handle the new error types.
10196
pub fn is_graceful_close(&self) -> bool {
10297
match self {
103-
Self::Session(session_err) => is_session_error_graceful(session_err),
104-
Self::Read(read_err) => {
105-
// ReadError::SessionError wraps SessionError
106-
if let web_transport::ReadError::SessionError(session_err) = read_err {
107-
return is_session_error_graceful(session_err);
98+
Self::WebTransport(wt_err) => match wt_err {
99+
web_transport::Error::Session(session_err) => {
100+
is_session_error_graceful(session_err)
108101
}
109-
false
110-
}
111-
Self::Write(write_err) => {
112-
// WriteError::SessionError wraps SessionError
113-
if let web_transport::WriteError::SessionError(session_err) = write_err {
114-
return is_session_error_graceful(session_err);
102+
web_transport::Error::Read(read_err) => {
103+
if let web_transport::quinn::ReadError::SessionError(session_err) = read_err {
104+
return is_session_error_graceful(session_err);
105+
}
106+
false
115107
}
116-
false
117-
}
108+
web_transport::Error::Write(write_err) => {
109+
if let web_transport::quinn::WriteError::SessionError(session_err) = write_err {
110+
return is_session_error_graceful(session_err);
111+
}
112+
false
113+
}
114+
_ => false,
115+
},
118116
_ => false,
119117
}
120118
}
@@ -129,26 +127,37 @@ impl From<SessionError> for serve::ServeError {
129127
}
130128
}
131129

132-
/// Helper to check if a `web_transport::SessionError` represents a graceful close.
130+
/// Helper to check if a `web_transport_quinn::SessionError` represents a graceful close.
133131
///
134-
/// This handles both:
135-
/// - Raw QUIC connections: `ApplicationClosed` with code 0
136-
/// - WebTransport connections: `ApplicationClosed` with HTTP/3 encoded code that decodes to 0
137-
fn is_session_error_graceful(err: &web_transport::SessionError) -> bool {
138-
use web_transport_quinn::SessionError;
132+
/// This handles:
133+
/// - WebTransport connections: `WebTransportError::Closed(0, _)` — web-transport-quinn v0.11+
134+
/// typically decodes HTTP/3-encoded close codes at this layer (when `SessionError` conversion
135+
/// applies), so graceful closes usually arrive here rather than as a raw
136+
/// `ConnectionError::ApplicationClosed`.
137+
/// - Raw QUIC connections: `ConnectionError::ApplicationClosed` with code 0
138+
/// - Local close: `ConnectionError::LocallyClosed`
139+
fn is_session_error_graceful(err: &web_transport::quinn::SessionError) -> bool {
140+
use web_transport::quinn::{SessionError, WebTransportError};
139141

140142
match err {
141143
SessionError::ConnectionError(conn_err) => is_connection_error_graceful(conn_err),
142-
// WebTransportError doesn't represent connection close in 0.3.x
144+
// WebTransport graceful close: peer sent close with code 0
145+
SessionError::WebTransportError(WebTransportError::Closed(0, _)) => true,
146+
// Other WebTransport errors (UnknownSession, read/write errors, non-zero close codes)
143147
SessionError::WebTransportError(_) => false,
144148
// SendDatagramError doesn't represent connection close
145149
SessionError::SendDatagramError(_) => false,
146150
}
147151
}
148152

149153
/// Helper to check if a `quinn::ConnectionError` represents a graceful close.
150-
fn is_connection_error_graceful(err: &quinn::ConnectionError) -> bool {
151-
use quinn::ConnectionError;
154+
///
155+
/// Note: In web-transport-quinn v0.11+, WebTransport `ApplicationClosed` with an HTTP/3-encoded
156+
/// close code is usually converted to `WebTransportError::Closed` during `SessionError` conversion
157+
/// when decoding succeeds. This function primarily handles raw QUIC (moqt:// ALPN) connections
158+
/// or non-decodable cases where the close code is not HTTP/3 encoded.
159+
fn is_connection_error_graceful(err: &web_transport::quinn::quinn::ConnectionError) -> bool {
160+
use web_transport::quinn::quinn::ConnectionError;
152161

153162
match err {
154163
ConnectionError::ApplicationClosed(close) => {
@@ -160,8 +169,9 @@ fn is_connection_error_graceful(err: &quinn::ConnectionError) -> bool {
160169
}
161170

162171
// Check for WebTransport code 0 (HTTP/3 encoded)
163-
// WebTransport code 0 maps to HTTP/3 code 0x52e4a40fa8db
164-
if let Some(wt_code) = web_transport_proto::error_from_http3(code) {
172+
// This is a fallback — in v0.11+, WebTransport closes are typically caught
173+
// by is_session_error_graceful's WebTransportError::Closed branch.
174+
if let Some(wt_code) = web_transport::quinn::proto::error_from_http3(code) {
165175
return wt_code == 0;
166176
}
167177

moq-transport/src/session/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ impl Session {
394394
/// Create an outbound/client QUIC connection, by opening a bi-directional QUIC stream for
395395
/// MOQT control messaging. Performs SETUP messaging and version negotiation.
396396
pub async fn connect(
397-
mut session: web_transport::Session,
397+
session: web_transport::Session,
398398
mlog_path: Option<PathBuf>,
399399
) -> Result<(Session, Publisher, Subscriber), SessionError> {
400400
let mlog = mlog_path.and_then(|path| {
@@ -447,7 +447,7 @@ impl Session {
447447
/// Accepts an inbound/server QUIC connection, by accepting a bi-directional QUIC stream for
448448
/// MOQT control messaging. Performs SETUP messaging and version negotiation.
449449
pub async fn accept(
450-
mut session: web_transport::Session,
450+
session: web_transport::Session,
451451
mlog_path: Option<PathBuf>,
452452
) -> Result<(Session, Option<Publisher>, Option<Subscriber>), SessionError> {
453453
let mut mlog = mlog_path.and_then(|path| {
@@ -673,7 +673,7 @@ impl Session {
673673
/// Will read stream header to know what type of stream it is and create
674674
/// the appropriate stream handlers.
675675
async fn run_streams(
676-
mut webtransport: web_transport::Session,
676+
webtransport: web_transport::Session,
677677
subscriber: Option<Subscriber>,
678678
) -> Result<(), SessionError> {
679679
let mut tasks = FuturesUnordered::new();
@@ -697,7 +697,7 @@ impl Session {
697697

698698
/// Receives QUIC datagrams and processes them using the Subscriber logic
699699
async fn run_datagrams(
700-
mut webtransport: web_transport::Session,
700+
webtransport: web_transport::Session,
701701
mut subscriber: Option<Subscriber>,
702702
) -> Result<(), SessionError> {
703703
loop {

moq-transport/src/session/reader.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl Reader {
6868
// We always read at least once to avoid an infinite loop if some dingus puts remain=0
6969
loop {
7070
let before_read = self.buffer.len();
71-
if !self.stream.read_buf(&mut self.buffer).await? {
71+
if self.stream.read_buf(&mut self.buffer).await?.is_none() {
7272
tracing::warn!(
7373
"[READER] decode: stream ended while waiting for data (have={} bytes, need={})",
7474
self.buffer.len(),
@@ -113,7 +113,7 @@ impl Reader {
113113
return Ok(Some(data));
114114
}
115115

116-
let chunk = self.stream.read_chunk(max).await?;
116+
let chunk = self.stream.read(max).await?;
117117
if let Some(ref data) = chunk {
118118
tracing::trace!("[READER] read_chunk: read {} bytes from stream", data.len());
119119
} else {
@@ -127,6 +127,6 @@ impl Reader {
127127
return Ok(false);
128128
}
129129

130-
Ok(!self.stream.read_buf(&mut self.buffer).await?)
130+
Ok(self.stream.read_buf(&mut self.buffer).await?.is_none())
131131
}
132132
}

0 commit comments

Comments
 (0)