-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: WebSockets over HTTP/2 #69669
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsBackground and motivationCurrently 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 that sup-porting WebSockets over HTTP/2 will give advantage of multiplexing connection. API Proposal // EXISTING
class ClientWebSocket : WebSocket
{
// NEW
public System.Version DefaultRequestVersion { get { throw null; } set { } };
public System.Net.Http.HttpVersionPolicy DefaultVersionPolicy { get { throw null; } set { } };
// EXISTING
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
// NEW
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, SocketsHttpHandler sharedHandler);
}
### API Usage
```csharp
var handler = new SocketsHttpHandler();
ClientWebSocket ws = new();
ws.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
ws.DefaultRequestVersion = HttpVersion.Version20;
ws.ConnectAsync(uri, cancellationToken, handler); Alternative DesignsWe may consider only one property RisksAdding new fields into
|
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. |
|
Can you elaborate on what you meant here?
Would the downgrade be handled by the |
I believe both alternatives are considered, but for YARP it will be better if it was handled by HTTP layer |
I agree with @stephentoub and @CarnaViire suggestions to rename policy and version properties and move it into // EXISTING
class ClientWebSocketOptions
{
// NEW
public System.Version RequestVersion { get { throw null; } set { } };
public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } };
} Would using
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. |
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. |
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 |
It should at least be a HttpMessageInvoker. |
One more API change: 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. |
The user can still create the method with
|
Next steps from the team discussion:
|
I think HttpRequestMessage needs a new Protocol field to represent the new pseudo header. WebTransport also uses this field. |
It would be great, I can reuse it to check if it's a WebSocket request. |
To sum up current API proposal: // EXISTING
class ClientWebSocket : WebSocket
{
// EXISTING
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
// NEW
public Task ConnectAsync(Uri uri, HttpMessageHandler sharedHandler, CancellationToken cancellationToken);
}
// EXISTING
class ClientWebSocketOptions
{
// NEW
public System.Version RequestVersion { get { throw null; } set { } };
public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } };
}
class HttpRequestMessage : System.IDisposable
{
// NEW
public string? Protocol { get { } set { } };
}
class HttpMethod : IEquatable<HttpMethod>
{
// EXISTING
// internal static HttpMethod Connect { get { throw null; } }
// NEW
public static HttpMethod Connect { get { throw null; } }
} |
HttpMessageInvoker should be easier for customers to use than HttpMessageHandler, they likely already have an HttpClient instance that they want to share with. Otherwise they have to hold onto an HttpMessageHandler and share that around as well. |
Updated proposal: class ClientWebSocket : WebSocket
{
// EXISTING
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
// NEW
public Task ConnectAsync(Uri uri, HttpMessageInvoker sharedHandler, CancellationToken cancellationToken);
}
// EXISTING
class ClientWebSocketOptions
{
// NEW
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Version Version { get { throw null; } set { } };
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } };
}
class HttpMethod : IEquatable<HttpMethod>
{
// EXISTING
// internal static HttpMethod Connect { get { throw null; } }
// NEW
public static HttpMethod Connect { get { throw null; } }
} Summary based on discussions with the team:
@stephentoub @Tratcher @CarnaViire @dotnet/ncl please let me know if I miss something |
The request header collection doesn't allow headers that start with ':' like ':protocol'. Will that be changed, or will this header be special cased? |
What does the downgrade flow look like? YARP will also need to implement this.
|
I tried to add this header for test purposes and didn't see issues with request. Also, we have ':authority' header there, is there any difference?
In case of failed h2 CONNECT request |
An error message is insufficient for targeted programmatic retries. There needs to be a unique derived exception type or some programmatically accessible state on the exception so we can identify the failure as the missing Extended CONNECT server setting (or lack of H2 ALPN?). We could take advantage of the Exception.Data collection if you don't want to add new public API. We wouldn't want to downgrade and retry for other types of failure like DNS or Socket issues. |
There is some related discussion in #43239 (comment) about extending the exception with protocol errors. |
If you try to add the
|
I would prefer using of
Following offline discussion I have to return to the
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 |
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? |
|
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. |
Team reached agreement, updating the proposal and switching it to ready-for-api-review. |
namespace System.Net.WebSockets
{
public partial class ClientWebSocket : WebSocket
{
// Existing
// public Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
public Task ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken);
}
public partial class ClientWebSocketOptions
{
public Version HttpVersion { get; [UnsupportedOSPlatform("browser")] set; };
public HttpVersionPolicy HttpVersionPolicy { get; [UnsupportedOSPlatform("browser")] set; };
}
}
namespace System.Net.Http
{
public partial class HttpMethod
{
public static HttpMethod Connect { get; }
}
}
namespace System.Net.Http.Headers
{
public partial class HttpRequestHeaders : HttpHeaders
{
public string? Protocol { get; set; };
}
} |
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
API Usage
Notes
HttpVersion
andHttpVersionPolicy
inClientWebSocketOptions
are similar toHttpClient
's property.ClientWebSocketOptions
for browser is a class with completely different properties, so the new properties don't affect it.Providing an external handler to
ClientWebSocket
is important because it enables advantage of HTTP/2 multiplexing by reusing the same handler for other ordinary HTTP/2 streams. Generic handlerHttpMessageInvoker
in ConnectAsync is a better alternative thanSocketsHttpHandler
. There is no need to check that it isSocketsHttpHandler
but it should be able to handle WebSocket requests and it is up to the user who provides it.The property for the
:protocol
header inHttpRequestHeaders
is required because we need to guarantee that pseudo headers are appeared before all regular headers based on spec.Other alternatives considered, but rejected:
SocketsHttpHandler
instead ofHttpMessageInvoker
.HttpVersion
if 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.
The text was updated successfully, but these errors were encountered: