Repository navigation
[API Proposal]: WebSockets over HTTP/2 #69669
Description
Background and motivation
Currently we are supporting WebSockets over HTTP/1.1 only. For WebSockets over HTTP/2 the protocol is quite different and described by RFC #8441. It is already supported by Chrome and there are an interest from YARP and ASP.NET partners. The main improvement is that supporting WebSockets over HTTP/2 will give advantage of multiplexing connection.
API Proposal
// EXISTING
class ClientWebSocket : WebSocket
{
// EXISTING
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
// NEW
public Task ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken);
}
// EXISTING
class ClientWebSocketOptions
{
// NEW
public System.Version HttpVersion { get { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
set { } };
public System.Net.Http.HttpVersionPolicy HttpVersionPolicy { get { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
set { } };
}
class HttpMethod : IEquatable<HttpMethod>
{
// EXISTING
// internal static HttpMethod Connect { get { throw null; } }
// NEW
public static HttpMethod Connect { get { throw null; } }
}
class HttpRequestHeaders : HttpHeaders
{
public string? Protocol { get { } set { } };
}API Usage
var handler = new SocketsHttpHandler();
ClientWebSocket ws = new();
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
ws.Options.HttpVersion = HttpVersion.Version20;
ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);
HttpRequestMessage request = new(HttpMethod.Connect, server.Address);
request.Headers.Protocol = "websocket";Notes
-
HttpVersionandHttpVersionPolicyinClientWebSocketOptionsare similar toHttpClient's property.ClientWebSocketOptionsfor browser is a class with completely different properties, so the new properties don't affect it. -
Providing an external handler to
ClientWebSocketis important because it enables advantage of HTTP/2 multiplexing by reusing the same handler for other ordinary HTTP/2 streams. Generic handlerHttpMessageInvokerin ConnectAsync is a better alternative thanSocketsHttpHandler. There is no need to check that it isSocketsHttpHandlerbut it should be able to handle WebSocket requests and it is up to the user who provides it. -
The property for the
:protocolheader inHttpRequestHeadersis required because we need to guarantee that pseudo headers are appeared before all regular headers based on spec.
Other alternatives considered, but rejected:
- Pass a low-level
SocketsHttpHandlerinstead ofHttpMessageInvoker. - Only one property
HttpVersionif we do not require downgrade handling. In that case we rely on the version the user provides.
Risks
Adding new fields might increase memory usage.
Activity
greenEkatherine commentedon May 23, 2022
Internal Changes
ClientWebSocketTo make downgrade for WebSocket from HTTP/2 to HTTP/1.1 possible we need to add additional header with the key because we cannot generate it later in


HttpClient. Other headers can be easily added and renamed to construct HTTP/1.1 WebSocket connection request from HTTP/2 ws request.HTTP/2 example:
HTTP/1.1 example:
HttpConnectionPoolSince YARP is using SocketsHttpHandler directly, we need to handle downgrade (if we decide to allow it) inside the
HttpConnectionPool. It will check the new setting ID of theHttp2ConnectionEnableConnect -- based on the server response.To make things easier we also should consider the new property
IsWebSocketRequestofHttpRequestMessage. It will check headers once to confirm that it is a websocket request (either h1 or h2) and we will not need to do it few times during downgrade.stephentoub commentedon May 23, 2022
Why "default"? We typically use that for statics providing a default value for any instance, but that's apparently not the case here.
This is concerning. It ties ClientWebSocket to the underlying implementation that should be used. ClientWebSocket has historically used HttpWebRequest, WinHttpHandler, and SocketsHttpHandler, and even today has a browser-based implementation for wasm.
CarnaViire commentedon May 23, 2022
RequestVersionandVersionPolicyfor connect request might look better as a part ofClientWebSocketOptions. But we might also want to have an additionalHttpVersionproperty onClientWebSocketitself if we support downgrade (so we could know what exactly was established)MihaZupan commentedon May 23, 2022
Can you elaborate on what you meant here?
Would the downgrade be handled by the
WebSocketlayer (i.e. calling into the handler multiple times)?Or did you mean that Websockets would be a special case the handler recognized to adapt headers as needed?
CarnaViire commentedon May 23, 2022
I believe both alternatives are considered, but for YARP it will be better if it was handled by HTTP layer
greenEkatherine commentedon May 23, 2022
I agree with @stephentoub and @CarnaViire suggestions to rename policy and version properties and move it into
ClientWebSocketOptions:Would using
HttpClientbe better from that perspective?Providing an external handler is a keypoint here because in that case we are able to take advantage of HTTP/2 multiplexing by reusing the same handler for other ordinary HTTP/2 streams.
stephentoub commentedon May 23, 2022
Understood. It doesn't change it being problematic, though. What happens if we decide we no longer want ClientWebSocket to be implemented with SocketsHttpHandler (which is an implementation detail, and which isn't even used in all builds)? That's not theoretical; we've already made such a change at least twice before with this type.
We should think through alternatives. If we ship the proposed API, that's it, forever-more this is implemented with SocketsHttpHandler.
greenEkatherine commentedon May 23, 2022
Based on the https://datatracker.ietf.org/doc/html/rfc8441#section-3 we should add a new SETTINGS parameter that a server sends to a client. My suggestion is to add a new property into
Http2Connectionto indicate this setting, so theHttpConnectionPoolwould be able to check its value of a connection and perform downgrade if needed.Tratcher commentedon May 23, 2022
It should at least be a HttpMessageInvoker.
greenEkatherine commentedon May 24, 2022
One more API change:
HttpMethod.Connectis internal but to construct external ws request (i.e. in YARP) it should be public:class HttpMethod : IEquatable<HttpMethod> { - internal static HttpMethod Connect { get { throw null; } } + public static HttpMethod Connect { get { throw null; } } }However, we need somehow allow its external usage for websockets only.
15 remaining items
greenEkatherine commentedon Jun 8, 2022
I would prefer using of
Exception.Data.Following offline discussion I have to return to the
Protocolproperty inHttpRequestMessage. The issue is not only with formatting of pseudo-header:protocol, more important is the order of pseudo-headers from the spec:With property we can guarantee the order. It also makes easier validation with reference to the protocol version because this header is not valid for HTTP/1.1.
@stephentoub what do you think about
Protocolproperty in this context?stephentoub commentedon Jun 8, 2022
No other header has a property on HttpRequestMessage. If we really want/need a dedicated property, shouldn't it be on one of the headers collection types, where the other strongly-typed headers live, e.g. on HttpRequestHeaders?
Tratcher commentedon Jun 8, 2022
:protocolis not a regular HTTP header, it's an h2 pseudo header. All other pseudo headers (method, scheme, path, authority) are derived from properties on the HttpRequestMessage. The same goes for:statuson the HttpResponseMessage.stephentoub commentedon Jun 8, 2022
Even so, I think it best maps to the HttpClient headers object model by being a part of the headers, e.g. request.Headers.Protocol rather than request.Protocol. I also like that doing so hides it a bit for HTTP/1.1 where it's irrelevant.
greenEkatherine commentedon Jun 16, 2022
Team reached agreement, updating the proposal and switching it to ready-for-api-review.
terrajobst commentedon Jun 28, 2022
Video