Background and Motivation
I am working on adding WebTransport support to Kestrel (PR #42097). That PR adds a few new public APIs.
Proposed API
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features;
/// <summary>
/// Controls the session and streams of a WebTransport session.
/// </summary>
+ public interface IWebTransportSession
{
/// <summary>
/// The id of the WebTransport session.
/// </summary>
+ public long SessionId { get; }
/// <summary>
/// Abruptly close the WebTransport session and stop all the streams.
/// </summary>
/// <param name="errorCode">HTTP error code that corresponds to the reason for causing the abort.</param>
/// <exception cref="Exception">If This is not a valid WebTransport session.</exception>
+ public void Abort(int errorCode = (int)Http3ErrorCode.NoError);
/// <summary>
/// Returns the next incoming stream in the order which Kestel received it. The stream can be either bidirectional or unidirectional.
/// </summary>
/// <remarks>To use WebTransport, you must first enable the Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams AppContextSwitch</remarks>
/// <exception cref="Exception">If this is not a valid WebTransport session or it fails to get a stream to accept.</exception>
/// <param name="cancellationToken">The cancellation token used to cancel the operation.</param>
/// <returns>The unidirectional or bidirectional stream that is next in the queue.</returns>
+ public ValueTask<WebTransportStream> AcceptStreamAsync(CancellationToken cancellationToken);
/// <summary>
/// Opens a new unidirectional output stream.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to cancel the operation.</param>
/// <exception cref="Exception">If This is not a valid WebTransport session.</exception>
/// <returns>The unidirectional stream that was opened.</returns>
+ public ValueTask<WebTransportStream> OpenUnidirectionalStreamAsync(CancellationToken cancellationToken);
}
namespace Microsoft.AspNetCore.Http.Features;
/// <summary>
/// API for accepting and retrieving WebTransport sessions.
/// </summary>
+ public interface IHttpWebTransportFeature
{
/// <summary>
/// Indicates if this request is a WebTransport request.
/// </summary>
+ public bool IsWebTransportRequest { get; }
/// <summary>
/// Accept the session request and allow streams to start being used.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel waiting for the session.</param>
/// <returns>An instance of a WebTransportSession which will be used to control the connection.</returns>
[RequiresPreviewFeatures("WebTransport is a preview feature")]
+ public ValueTask<IWebTransportSession> AcceptAsync(CancellationToken cancellationToken);
}
namespace Microsoft.AspNetCore.Server.Kestrel.Core;
/// <summary>
/// Represents a base WebTransport stream. Do not use directly as it does not
/// contain logic for handling data.
/// </summary>
+public class WebTransportStream : Stream
{
/// <summary>
/// Hard abort the stream and cancel data transmission.
/// </summary>
/// <param name="errorCode"> the error code to pass into the logs</param>
+ public void Abort(int errorCode = (int)Http3ErrorCode.NoError);
}
Usage Examples
For a nearly-exhaustive piecewise view of how this API can be used you can refer to this doc: https://github.com/dotnet/aspnetcore/blob/c5e66c255c0254a9be191ee53031cec6b79def48/docs/WebTransport.md
Example 1
This example waits for a bidirectional stream. Once it receives one, it will read the data from it, reverse it and then write it back to the stream.
public void Configure(IApplicationBuilder app)
{
var memory = new Memory<byte>(new byte[4096]);
app.Use(async (context, next) =>
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (feature.IsWebTransportRequest)
{
// accept a new session
var session = await feature.AcceptAsync(CancellationToken.None);
WebTransportStream stream;
do
{
// wait until we get a bidirectional stream
stream = await session.AcceptStreamAsync(CancellationToken.None);
} while (stream.CanRead && stream.CanWrite);
// read some data from the stream
var length = await stream.ReadAsync(memory, CancellationToken.None);
// do some operations on the contents of the data
memory.Span.Reverse();
// write back the data to the stream
await stream.WriteAsync(memory, CancellationToken.None);
await stream.FlushAsync(CancellationToken.None);
}
else
{
await next(context);
}
});
}
Example 2
This example opens a new stream from the server side and then sends data.
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (feature.IsWebTransportRequest)
{
// accept a new session
var session = await feature.AcceptAsync(CancellationToken.None);
// open a new stream from the server to the client
var stream = await session.OpenUnidirectionalStreamAsync(CancellationToken.None);
// write data to the stream
await stream.WriteAsync(new Memory<byte>(new byte[] { 65, 66, 67, 68, 69 }), CancellationToken.None);
await stream.FlushAsync(CancellationToken.None);
}
else
{
await next(context);
}
});
}
Alternative Designs
There are many other possible designs such as using Action and Func parameters when accepting and openning streams and sessions. Then those could be called when a new session or stream is openned instead of the async/await design. However, most of ASP.NET seems to be using async/await.
I also looked into making the WebTransportStream extend from the Http3Stream class. However, as WebTransport streams don't need all the framing logic, it just complicated the design. Also, technically WebTransport is defined over HTTP/2 as well. So, this would be incorrect if we wanted to add WebTransport over HTTP/2 in the future.
Lastly, I also looked into separating the WebTransportStream class into a WebTransportinputStream, WebTransportOutputStream and a WebTransportBidirectionalStream. This caused a lot of code duplication and unnecessary complexity though. Moreover, it caused me to need to restructure the HTTP/3 stream handling logic as non-bidirectional HTTP/3 streams are assumed to be control streams. This new proposed solution avoids all this added complexity.
Risks
As WebTransport is hidden behind an AppContext switch and behind a PreviewFeature annotation, the risks are only enabled once the user specifically opts-in. Having said that, this API is only adding new functionality and trying to minimize the changes to existing functionality.
Background and Motivation
I am working on adding WebTransport support to Kestrel (PR #42097). That PR adds a few new public APIs.
Proposed API
Usage Examples
For a nearly-exhaustive piecewise view of how this API can be used you can refer to this doc: https://github.com/dotnet/aspnetcore/blob/c5e66c255c0254a9be191ee53031cec6b79def48/docs/WebTransport.md
Example 1
This example waits for a bidirectional stream. Once it receives one, it will read the data from it, reverse it and then write it back to the stream.
Example 2
This example opens a new stream from the server side and then sends data.
Alternative Designs
There are many other possible designs such as using
ActionandFuncparameters when accepting and openning streams and sessions. Then those could be called when a new session or stream is openned instead of theasync/awaitdesign. However, most of ASP.NET seems to be usingasync/await.I also looked into making the
WebTransportStreamextend from theHttp3Streamclass. However, as WebTransport streams don't need all the framing logic, it just complicated the design. Also, technically WebTransport is defined over HTTP/2 as well. So, this would be incorrect if we wanted to add WebTransport over HTTP/2 in the future.Lastly, I also looked into separating the
WebTransportStreamclass into aWebTransportinputStream,WebTransportOutputStreamand aWebTransportBidirectionalStream. This caused a lot of code duplication and unnecessary complexity though. Moreover, it caused me to need to restructure the HTTP/3 stream handling logic as non-bidirectional HTTP/3 streams are assumed to be control streams. This new proposed solution avoids all this added complexity.Risks
As WebTransport is hidden behind an
AppContextswitch and behind aPreviewFeatureannotation, the risks are only enabled once the user specifically opts-in. Having said that, this API is only adding new functionality and trying to minimize the changes to existing functionality.