[WebTransport] half-closed streams

50 views
Skip to first unread message

Luke Curley

unread,
Sep 11, 2020, 12:26:36 AM9/11/20
to web-transport-dev
Hey folks,

I recently filed a bug against Chromium, and apparently it's caused by the QuicTransport implementation not supporting half-closed streams. That means that the stream is closed when either the reader or writer is closed.

As far as I can tell, the W3C spec doesn't mention anything while the WebTransport overview defers to QUIC's stream state. QUIC itself supports half-closed streams and it's important. For example, HTTP/3 uses the half-closure of a stream to denote the end of a request. My broken code was doing something similar but with a JSON request/response.

Is this the intended behavior or just an oversight in the implementation?


Thanks!

Lucas Pardue

unread,
Sep 11, 2020, 1:21:27 PM9/11/20
to Luke Curley, web-transport-dev
I agree that this seems problematic if it is intentional. 

--
You received this message because you are subscribed to the Google Groups "web-transport-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web-transport-...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/web-transport-dev/CAHVo%3DZ%3D6nU80%3DATj12u_mfEtG49vJmk3tnJDw8wuY4iq5csFCw%40mail.gmail.com.

Bernard Aboba

unread,
Sep 11, 2020, 1:49:22 PM9/11/20
to Lucas Pardue, Luke Curley, web-transport-dev
Lucas said: 

"I agree that this seems problematic if it is intentional."

[BA] Looking back at the archives,  it appears that support for half-closed connections was lost when the API was updated to use WHATWG Streams. 

Prior to that point, when the API was based on ORTC (the version of the API supported in the RTCQuicTransport Trial of 2019), half-closed connections appear to have been supported:

In that draft, bi-directional streams had both a writable and readable internal slot, corresponding to the state of each direction.  The abortWriting() method sends a RST_STREAM frame, and the abortReading() method sends a STOP_SENDING frame. 

In that github repo, I also found a state transition diagram provided by Seth Hampson in order to clarify the half-closed behavior: 



Bernard Aboba

unread,
Sep 11, 2020, 2:02:34 PM9/11/20
to Lucas Pardue, Luke Curley, web-transport-dev
Just to clarify, the definition of abortWriting() and abortReading() methods didn't change with the move to WHATWG Streams, but the implementation behavior may have. 

The figure that Seth provided to clarify half-closed behavior can be seen better here: 

That figure refers to both finish() and reset() mechanisms, a distinction which was made to better model half-closed behavior. 




Luke Curley

unread,
Sep 11, 2020, 3:22:29 PM9/11/20
to Bernard Aboba, Lucas Pardue, web-transport-dev
I went through and enumerated the relevant BidirectionalStream methods and how I think they should map to QUIC:

abortReading() : send STOP_SENDING with code
abortWriting() : send RESET_STREAM with code 

writable.abort()             : send RESET_STREAM  
writable.close()             : send STREAM_FINAL  
writable.getWriter().write() : send STREAM
writable.getWriter().close() : send STREAM_FINAL
writable.getWriter().abort() : send RESET_STREAM

readable.cancel()             : send STOP_SENDING  
readable.getReader().read()   : receive STREAM or STREAM_FINAL 
readable.getReader().cancel() : send STOP_SENDING


Unless I'm mistaken, there's quite a few ways to close a stream... It's another discussion but it might be worth consolidating abortReading() and abortWriting() into WHATWG streams' cancel() and abort() respectively.


Here's what I have tested and observed in the Chrome implementation:

writable.getWriter().write() : send STREAM
writable.getWriter().close() : send STREAM_FINAL, RESET_STREAM with code 268?, and STOP_SENDING with code 6?
readable.getReader().read()  : receive STREAM or STREAM_FINAL


The writer.close() method is quite broken. Even with a unidirectional stream, calling writer.close() will send a RESET_STREAM that will cause data loss if there's any packet loss or reordering. And with a bidirectional stream, it also sends a STOP_SENDING that will close the reader half of the stream like mentioned. Both of these frames should only be sent on abort() or cancel() respectively.


Bernard Aboba

unread,
Sep 11, 2020, 4:45:41 PM9/11/20
to Luke Curley, Lucas Pardue, web-transport-dev
Luke --

Going through the WebTransport spec, here is the behavior that appears to be specified in various sections relating to closing or aborting. 

Taking inventory: 

QuicTransport interfaces

BiDirectionalStream or ReceiveStream.abortReading() : send STOP_SENDING with errorCode. (see: https://wicg.github.io/web-transport/#dom-incomingstream-abortreading).  Note typo that refers to "abortwriting" instead of "abortreading" in this section. 
BiDirectionalStream or SendStream.abortWriting() : send RESET_STREAM with errorCode  (see: https://wicg.github.io/web-transport/#dom-outgoingstream-abortwriting)

QuicTransport.close()             : send CONNECTION_CLOSE with errorCode, reason (see: https://wicg.github.io/web-transport/#dom-webtransport-close

There is also WHATWG streams behavior from the cancel() and abort() methods which isn't mentioned in the document.  It would seem reasonable to clarify that they are mapped as you indicated: 

writable.abort()             : send RESET_STREAM  
writable.getWriter().abort() : send RESET_STREAM

readable.cancel()             : send STOP_SENDING  
readable.getReader().cancel() : send STOP_SENDING

and yes, all of this does imply quite a few ways to close a stream.  Not sure it is too terrible assuming it is made clear what everything does. 

QuicTransport interfaces

[Exposed=(Window,Worker)]

interface QuicTransport {

  constructor(USVString url, optional QuicTransportOptions options = {});

  readonly attribute unsigned short maxDatagramSize;

  readonly attribute WebTransportState state;

  readonly attribute Promise<WebTransportCloseInfo> closed;

  attribute EventHandler onstatechange;

  Promise<QuicTransportStats> getStats();

  Promise<SendStream> createSendStream(optional SendStreamParameters parameters = {});

  ReadableStream receiveStreams();

  Promise<BidirectionalStream> createBidirectionalStream();

  ReadableStream receiveBidirectionalStreams();

  WritableStream sendDatagrams();

  ReadableStream receiveDatagrams();

  void close(optional WebTransportCloseInfo closeInfo = {});

};


interface BiDirectionalStream {

  readonly attribute WritableStream writable;

  readonly attribute Promise<StreamAbortInfo> writingAborted;

  readonly attribute ReadableStream readable;

  readonly attribute Promise<StreamAbortInfo> readingAborted;

  void abortWriting(optional StreamAbortInfo abortInfo = {});

  void abortReading(optional StreamAbortInfo abortInfo = {});

  Promise<ArrayBuffer> arrayBuffer();

}


interface SendStream {

  readonly attribute WritableStream writable;

  readonly attribute Promise<StreamAbortInfo> writingAborted;

  void abortWriting(optional StreamAbortInfo abortInfo = {});

}


interface ReceiveStream {

  readonly attribute ReadableStream readable;

  readonly attribute Promise<StreamAbortInfo> readingAborted;

  void abortReading(optional StreamAbortInfo abortInfo = {});

  Promise<ArrayBuffer> arrayBuffer();

}



Luke Curley

unread,
Sep 11, 2020, 5:27:20 PM9/11/20
to Bernard Aboba, Lucas Pardue, web-transport-dev
Yep that's all accurate. Although you missed WritableStream.close(), which should behave like an end of stream marker (clean termination). This means sending a STREAM frame with the final bit sent and not a RESET_STREAM.

I think it would be a good idea to explicitly map the WHATWG stream methods to specific QUIC operations. I don't think there's any incompatibilities and they both have a surprisingly similar API.
Reply all
Reply to author
Forward
0 new messages