Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/aeronet_webtransport/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ wtransport = { workspace = true }
x509-cert = { workspace = true }
xwt-wtransport = { workspace = true }

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt", "net", "time"] }

[target.'cfg(target_family = "wasm")'.dependencies]
js-sys = { workspace = true }
wasm-bindgen = { workspace = true }
Expand Down
75 changes: 69 additions & 6 deletions crates/aeronet_webtransport/src/client/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@ use {
xwt_core::prelude::*,
};

// Dropping a Rust future does not cancel a JavaScript `ready` promise.
// Explicitly close the browser transport unless ownership reaches Session.
#[cfg(target_family = "wasm")]
struct PendingWebTransport(Option<xwt_web::web_wt_sys::WebTransport>);

#[cfg(target_family = "wasm")]
impl Drop for PendingWebTransport {
fn drop(&mut self) {
if let Some(transport) = self.0.take() {
transport.close();
}
}
}

pub async fn start(
config: ClientConfig,
target: ConnectTarget,
tx_next: oneshot::Sender<ToConnected>,
mut tx_next: oneshot::Sender<ToConnected>,
) -> Result<Never, DisconnectReason> {
let endpoint = {
#[cfg(target_family = "wasm")]
Expand All @@ -35,16 +49,20 @@ pub async fn start(
};
debug!("Created endpoint");

let conn = {
let connecting = async {
#[cfg(target_family = "wasm")]
{
endpoint
let connecting = endpoint
.connect(&target)
.await
.map_err(|err| ClientError::Connect(err.into()))?
.map_err(|err| ClientError::Connect(err.into()))?;
let mut pending = PendingWebTransport(Some(connecting.transport.clone()));
let session = connecting
.wait_connect()
.await
.map_err(|err| ClientError::AwaitConnect(err.into()))?
.map_err(|err| ClientError::AwaitConnect(err.into()))?;
pending.0 = None;
Ok::<_, ClientError>(session)
}

#[cfg(not(target_family = "wasm"))]
Expand All @@ -54,7 +72,18 @@ pub async fn start(
.connect(target)
.await
.map(xwt_wtransport::Connection)
.map_err(ClientError::Connect)?
.map_err(ClientError::Connect)
}
};
// Cancellation must also cover the handshake, before SessionBackend has
// a disconnect channel. Dropping the frontend's receiver cancels this
// future and drops the native endpoint rather than leaving a retired task.
let conn = {
let cancelled = tx_next.cancellation();
futures::pin_mut!(connecting, cancelled);
match futures::future::select(cancelled, connecting).await {
futures::future::Either::Left(_) => return Err(SessionError::FrontendClosed.into()),
futures::future::Either::Right((result, _)) => result?,
}
};
debug!("Connected");
Expand Down Expand Up @@ -92,3 +121,37 @@ pub async fn start(
debug!("Starting session loop");
Err(backend.start().await)
}

#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use {super::*, core::time::Duration, wtransport::endpoint::IntoConnectOptions};

#[test]
fn dropping_frontend_cancels_pending_handshake_without_waiting_for_network_timeout() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
// Bound but silent UDP peer: no connection-refused shortcut.
let peer = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let config = ClientConfig::builder()
.with_bind_default()
.with_native_certs()
.max_idle_timeout(Some(Duration::from_secs(5)))
.unwrap()
.build();
Comment on lines +138 to +143
let (tx, rx) = oneshot::channel();
let target = format!("https://{}/", peer.local_addr().unwrap()).into_options();
let task = tokio::spawn(start(config, target, tx));
tokio::task::yield_now().await;
drop(rx);
let result = tokio::time::timeout(Duration::from_millis(500), task).await;
assert!(
result.is_ok(),
"abandoned handshake continued running after fallback"
);
assert!(result.unwrap().unwrap().is_err());
});
}
}