Skip to content

QuicStream default error code API limitation #121910

@PatrikTrefil

Description

@PatrikTrefil

Currently, we can set a default stream error code for all QuicStream instances associated with a QuicConnection. This default value must be set using QuicConnectionOptions.DefaultStreamErrorCode.

In some scenarios, it may be useful to be able to set default stream error codes per stream and also to be able to modify the value during the lifetime of the object. The following paragraph will explain why.

The QuicConnectionOptions.DefaultStreamErrorCode is used by System.Net.Http when setting up a QUIC connection for HTTP/3. It uses the value (long)Http3ErrorCode.RequestCancelled. This is built on the assumption, that when the stream's read/write side needs to be closed internally, it always means an HTTP/3 request is cancelled. This assumption is true now, but we might not be able to rely on it the future. If support for WebTransport over HTTP/3 is added, we run into problems. The way the protocol works is that a QUIC connection is shared between the HTTP/3 protocol and the WebTransport over HTTP/3 protocol. If we have a QUIC stream that is used by WebTransport over HTTP/3 and we need to close the read/write side internally, the HTTP/3 error code is used, which is an invalid stream error code in the WebTransport over HTTP/3 protocol. Currently, there are two scenarios when this might happen. The first one is during disposal of a WebTransport stream, which works as a wrapper around a QUIC stream. In this scenario the solution is simple - we just call QuicStream.Abort with the desired error code before we call QuicStream.Dispose. The second scenario when the default error code is applied is during operation cancellations. Consider the following code:

WebTransportStream stream = ...;
byte[] data = ...;
CancellationTokenSource cts = new();
ValueTask writeTask = stream.WriteAsync(data, cts.Token);
cts.Cancel();

If WebTransportStream.WriteAsync just passed the cancellation token to QuicStream.WriteAsync, the result would be that the write side is closed using the HTTP/3 error code. This behavior is invalid in WebTransport over HTTP/3. Therefore, the WebTransport over HTTP/3 implementation must work around this as follows:

class WebTransportStream : Stream {
    public override ValueTask WriteAsync(byte[] data, CancellationToken cancellationToken) {
        CancellationTokenRegistration ctr? = null;
        if (cancellationToken.CanBeCanceled)
        {
            ctr = cancellationToken.Register(() =>
            {
                _quicStream.Abort(abortDirection, _remappedDefaultStreamErrorCode);
            });
        }
        try
        {
            await _quicStream.WriteAsync(buffer.AsMemory(offset, count)).ConfigureAwait(false);
        }
        catch (QuicException ex)
        {
            / * exception handling * /
        }
        finally
        {
            if (ctr.HasValue)
            {
                await ctr.Value.DisposeAsync().ConfigureAwait(false);
            }
        }
    }
}

The same handling of the cancellation token is required for all async operations that support a cancellation token. If we were able to assign a default stream error code for every instance of a QuicStream, the code would be much simpler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions