fix(nests): reply SubscribeDrop on unsupported tracks instead of silent FIN
When a relay opens a SUBSCRIBE bidi for a track this session doesn't publish (e.g. a watcher subscribes to `video/data` against a broadcast that only declares `audio/data` + `catalog.json`), the session was silently FINing the bidi without writing any reply. The peer's wait on its receive side resolves to end-of-stream with no indication WHY — indistinguishable from "publisher disappeared mid-subscribe." Reply with [MoqLiteSubscribeDrop] carrying [MoqLiteSubscribeDropCode.TRACK_DOES_NOT_EXIST] (0x04, mirroring the IETF-MoQ `ErrorCode.TRACK_DOES_NOT_EXIST` convention) and an informational reason phrase listing the tracks we DO publish, then FIN. Matches what kixelated's `rs/moq-lite/src/lite/subscribe.rs` expects on this path. Doesn't change the happy path — every track NestsUI-v2 subscribes to (`audio/data` + `catalog.json`) is now published — but is the spec-correct behaviour for any future watcher that prospectively subscribes to a track our publisher hasn't declared. https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+44
@@ -357,6 +357,50 @@ class MoqLiteSessionTest {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publisher_replies_subscribeDrop_when_track_is_not_published() =
|
||||
runBlocking {
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
// Publisher serves audio/data only.
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
|
||||
|
||||
// Relay opens a Subscribe bidi for a DIFFERENT track. The
|
||||
// session must reply with SubscribeDrop carrying the
|
||||
// TRACK_DOES_NOT_EXIST code rather than a silent FIN —
|
||||
// otherwise the watcher's response wait resolves only when
|
||||
// the bidi is FIN'd, with no indication WHY (looks
|
||||
// identical to "publisher disappeared mid-subscribe").
|
||||
val subBidi = serverSide.openBidiStream()
|
||||
subBidi.write(Varint.encode(MoqLiteControlType.Subscribe.code))
|
||||
subBidi.write(
|
||||
MoqLiteCodec.encodeSubscribe(
|
||||
MoqLiteSubscribe(
|
||||
id = 99L,
|
||||
broadcast = "speakerPubkey",
|
||||
track = "video/data",
|
||||
priority = 0x80,
|
||||
ordered = true,
|
||||
maxLatencyMillis = 0L,
|
||||
startGroup = null,
|
||||
endGroup = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val ackChunk = withTimeout(2_000) { subBidi.incoming().first() }
|
||||
val resp = MoqLiteCodec.decodeSubscribeResponse(ackChunk)
|
||||
val dropped = resp as MoqLiteCodec.SubscribeResponse.Dropped
|
||||
assertEquals(MoqLiteSubscribeDropCode.TRACK_DOES_NOT_EXIST, dropped.drop.errorCode)
|
||||
// Reason phrase is informational; pin substring rather than
|
||||
// the exact text so we can keep tweaking the wording.
|
||||
kotlin.test.assertContains(dropped.drop.reasonPhrase, "video/data")
|
||||
|
||||
publisher.close()
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publisher_close_emits_ended_announce() =
|
||||
runBlocking {
|
||||
|
||||
Reference in New Issue
Block a user