Code change:
3.2. <del> The first stream opened is a client-initiated bidirectional stream where the peers exchange SETUP messages (message-setup).  The subsequent streams MAY be either unidirectional or bidirectional. For exchanging content, an application would typically send a unidirectional stream containing a single OBJECT message (message- object), as putting more than one object into one stream may create head-of-line blocking delays.  However, if one object has a hard dependency on another object, putting them on the same stream could be a valid choice. </del> <ins> The first stream opened is a client-initiated bidirectional control stream where the peers exchange SETUP messages (message-setup).  All messages defined in this draft are sent on the control stream after the SETUP message.  Control messages MUST NOT be sent on any other stream, and a peer receiving a control message on a different stream closes the session as a 'Protocol Violation'.  Objects MUST NOT be sent on the control stream, and a peer receiving an Object on the control stream closes the session as a 'Protocol Violation'. This draft only specifies a single use of bidirectional streams. Objects are sent on unidirectional streams.  Because there are no other uses of bidirectional streams, a peer MAY currently close the connection if it receives a second bidirectional stream. The control stream MUST NOT be abruptly closed at the QUIC layer. Doing so results in the session being closed as a 'Protocol Violation'. </ins> 3.3.

Related discussion:
text: There's an assumption in the current draft that some messages are partially reliable (OBJECT messages) while some are fully reliable (everything else, aka control messages). The ability to drop a message depend on the ability to reset the QUIC stream. However, reseting a stream containing control messages wrecks havok on the subscription state machine. There's no way within QUIC determine if a messages was received or processed prior to a reset. For example, if I send a SUBSCRIBE on stream and then a receive a STOPSENDING for that stream, then the subscription enters a limbo state: The SUBSCRIBE was processed and a SUBSCRIBEOK or SUBSCRIBEERROR is forthcoming. The SUBSCRIBE was never processed and a SUBSCRIBEOK or SUBSCRIBEERROR will never be received. The only option currently is to set a timer after a reset is received. After a few RTTs or so, we assume the SUBSCRIBE was lost and issue a new SUBSCRIBE on a new stream, introducing latency in the process. Though things get extra weird if we receive a response after this timeout expires... I think we either need to state: Control streams MUST NOT be reset (like HTTP/3) Reseting a control stream also resets all prior messages (ie. all prior SUBSCRIBEs). Reseting a control stream resets all unacknowledged messages (ie. SUBSCRIBEs without SUBSCRIBEOK). Option 3 is problematic, since the subscriber knows that the OK was received prior to the reset but the publisher doesn't. Personally, I think option 1 is the best is there's a single control stream, while option 2 is the best if there's multiple (scoped) control streams. Regardless of the approach, data messages and control messages should be split () because they have different reliability. A receiver should know if it can issue STOP_SENDING for an OBJECT without impacting control messages, especially if we take approach 1. My application uses a bidirectional stream for control messages and unidirectional streams for data messages. It makes it very easy for the application to know if a stream is resettable just based on the ID.
From , (s/channels/streams) -- snip Control and Data Channels When a endpoint client or relay begins a transaction for media delivery, a new bilateral QUIC Stream is opened.