open-standards/joule-code/joule-code-rs/crates/joule-moq-relay/tests/request_rejection.rs
Transaction Science 046a93f7e6 joule-moq-relay: a rejected request is answered REQUEST_ERROR and FIN, never stopped
The interop run of 11 November (D30) saw a STOP_SENDING beside every REQUEST_ERROR. The relay
answered a request it rejected with REQUEST_ERROR and a FIN, then dropped the direction it was
reading, and dropping a receiving direction before the peer's FIN sends STOP_SENDING (QUIC on a
stream not read to its end; the WebSocket binding's WT_STOP_SENDING). -22 §6.4.2.3: requests
are cancelled "using RESET_STREAM for a direction they are sending and STOP_SENDING for a
direction they are receiving", and "When an endpoint rejects a request without performing any
application processing, it SHOULD send a REQUEST_ERROR and FIN the stream" (the same at -23).

wire.rs: a request stream on which this end sent a REQUEST_ERROR (ReqTx::send marks it) is,
when its reader is dropped before the peer's FIN, read to that FIN for at most 10 s
(REJECTED_LINGER) and only then stopped. Every other drop is as before.

tests/request_rejection.rs: a TRACK_STATUS for a track nobody publishes, over native QUIC and
the WebSocket binding, is answered REQUEST_ERROR and FIN; the requester then FINs and its
direction was never stopped. Falsified: with the stream never marked rejected, the requester's
direction is stopped (STOP_SENDING, code 0) and the test fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 16:40:49 -05:00

44 lines
2.6 KiB
Rust

//! A request the relay rejects is answered REQUEST_ERROR and FIN, and not cancelled
//! (`draft-ietf-moq-transport-22` §6.4.2.3: "When an endpoint rejects a request without
//! performing any application processing, it SHOULD send a REQUEST_ERROR and FIN the stream";
//! a request is cancelled by RESET_STREAM and STOP_SENDING). Over native QUIC and the WebSocket
//! binding, a TRACK_STATUS for a track nobody publishes is answered REQUEST_ERROR and FIN; the
//! requester then sends its own FIN, and its direction was never stopped: no STOP_SENDING came
//! beside the REQUEST_ERROR. The relay used to drop the direction it was reading as it
//! answered, and its transport stopped it.
mod common;
use common::*;
use joule_moq::draft22::{ControlMessage, Parameters, TrackRequest};
use joule_moq_relay::client::ClientOptions;
use joule_moq_relay::wire::MessageReader;
use std::time::Duration;
use tokio::time::timeout;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_rejected_request_is_answered_and_finished_never_stopped() {
timeout(Duration::from_secs(60), run()).await.expect("the test finishes in time");
}
async fn run() {
let server = relay(|_| {}).await;
for via in [Via::Quic, Via::WebSocket] {
let (c, _endpoint) = connect(&server, via, ClientOptions::default()).await;
let (mut send, recv) = timeout(STEP, c.conn.open_bi()).await.expect("in time").expect("a request stream");
let ask = ControlMessage::TrackStatus(TrackRequest { request_id: c.next_request_id(), namespace: ns(&["nobody", "publishes"]), name: name("this"), params: Parameters::new() });
send.write_all(&ask.encode().unwrap()).await.expect("the request goes");
let mut reader = MessageReader::new(recv);
let answer = timeout(STEP, reader.next()).await.expect("an answer in time").expect("it reads");
assert!(matches!(answer, Some(ControlMessage::RequestError(_))), "{via:?}: {answer:?}");
let after = timeout(STEP, reader.next()).await.expect("the relay's FIN in time").expect("it reads");
assert!(after.is_none(), "{via:?}: the relay's direction ends after its REQUEST_ERROR");
// Time for a STOP_SENDING the relay might send to arrive, then this end's own FIN.
tokio::time::sleep(Duration::from_millis(300)).await;
send.finish().expect("the requester's FIN");
let stopped = timeout(STEP, send.delivered()).await.expect("acknowledged in time").expect("no transport error");
assert_eq!(stopped, None, "{via:?}: the relay stopped the requester's direction (STOP_SENDING) beside its REQUEST_ERROR");
c.close(0, "done");
}
server.shutdown().await;
}