Skip to content

[API Proposal]: WebSockets over HTTP/2 #69669

Closed

Description

@greenEkatherine

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

  • HttpVersion and HttpVersionPolicy in ClientWebSocketOptions are similar to HttpClient'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 handler HttpMessageInvoker in ConnectAsync is a better alternative than SocketsHttpHandler. There is no need to check that it is SocketsHttpHandler 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 in HttpRequestHeaders is 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 SocketsHttpHandler instead of HttpMessageInvoker.
  • Only one property 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.

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 23, 2022
  2. greenEkatherine commented on May 23, 2022

    @greenEkatherine
    ContributorAuthor

    Internal Changes

    ClientWebSocket

    To 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:
    image
    HTTP/1.1 example:
    image

    HttpConnectionPool

    Since 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 the Http2Connection EnableConnect -- based on the server response.

    To make things easier we also should consider the new property IsWebSocketRequest of HttpRequestMessage. 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.

  3. stephentoub commented on May 23, 2022

    @stephentoub
    Member

    public System.Version DefaultRequestVersion { get { throw null; } set { } };
    public System.Net.Http.HttpVersionPolicy DefaultVersionPolicy { get { throw null; } set { } };

    Why "default"? We typically use that for statics providing a default value for any instance, but that's apparently not the case here.

        public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, SocketsHttpHandler sharedHandler);
    

    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.

  4. CarnaViire commented on May 23, 2022

    @CarnaViire
    Member

    RequestVersion and VersionPolicy for connect request might look better as a part of ClientWebSocketOptions. But we might also want to have an additional HttpVersion property on ClientWebSocket itself if we support downgrade (so we could know what exactly was established)

  5. MihaZupan commented on May 23, 2022

    @MihaZupan
    Member

    It will check the new setting ID of the Http2Connection EnableConnect

    Can you elaborate on what you meant here?

    To 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.

    Would the downgrade be handled by the WebSocket layer (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?

  6. CarnaViire commented on May 23, 2022

    @CarnaViire
    Member

    Would the downgrade be handled by the WebSocket layer (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?

    I believe both alternatives are considered, but for YARP it will be better if it was handled by HTTP layer

  7. greenEkatherine commented on May 23, 2022

    @greenEkatherine
    ContributorAuthor

    I agree with @stephentoub and @CarnaViire suggestions to rename policy and version properties and move it into ClientWebSocketOptions:

        // EXISTING
        class ClientWebSocketOptions
        {
            // NEW
            public System.Version RequestVersion { get { throw null; } set { } };
            public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } };
        }

    Would using HttpClient be better from that perspective?

        public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, SocketsHttpHandler sharedHandler);
    

    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.

    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.

  8. stephentoub commented on May 23, 2022

    @stephentoub
    Member

    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.

  9. greenEkatherine commented on May 23, 2022

    @greenEkatherine
    ContributorAuthor

    It will check the new setting ID of the Http2Connection EnableConnect

    Can you elaborate on what you meant here?

    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 Http2Connection to indicate this setting, so the HttpConnectionPool would be able to check its value of a connection and perform downgrade if needed.

  10. Tratcher commented on May 23, 2022

    @Tratcher
    Member
        public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, SocketsHttpHandler sharedHandler);
    

    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.

    It should at least be a HttpMessageInvoker.

  11. greenEkatherine commented on May 24, 2022

    @greenEkatherine
    ContributorAuthor

    One more API change: HttpMethod.Connect is 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.

  12. 15 remaining items

  13. greenEkatherine commented on Jun 8, 2022

    @greenEkatherine
    ContributorAuthor

    We could take advantage of the Exception.Data collection if you don't want to add new public API.

    I would prefer using of Exception.Data.

    If you try to add the :protocol header manually you will see an exception because the name is invalid (: is not a valid character for the name).

    Following offline discussion I have to return to the Protocol property in HttpRequestMessage. The issue is not only with formatting of pseudo-header :protocol, more important is the order of pseudo-headers from the spec:

    All pseudo-header fields MUST appear in a field block before all regular field lines. Any request or response that contains a pseudo-header field that appears in a field block after a regular field line MUST be treated as malformed

    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 Protocol property in this context?

  14. stephentoub commented on Jun 8, 2022

    @stephentoub
    Member

    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?

  15. Tratcher commented on Jun 8, 2022

    @Tratcher
    Member
    HEADERS + END_HEADERS
    :method = CONNECT
    :protocol = websocket
    :scheme = https
    :path = /chat
    :authority = server.example.com
    sec-websocket-protocol = chat, superchat
    sec-websocket-extensions = permessage-deflate
    sec-websocket-version = 13
    origin = http://www.example.com
    

    :protocol is 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 :status on the HttpResponseMessage.

  16. stephentoub commented on Jun 8, 2022

    @stephentoub
    Member

    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.

  17. added
    blockingMarks issues that we want to fast track in order to unblock other important work
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jun 16, 2022
  18. greenEkatherine commented on Jun 16, 2022

    @greenEkatherine
    ContributorAuthor

    Team reached agreement, updating the proposal and switching it to ready-for-api-review.

  19. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Jun 17, 2022
  20. removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Jun 21, 2022
  21. terrajobst commented on Jun 28, 2022

    @terrajobst
    Contributor

    Video

    • Looks good as proposed
    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; };
        }
    }
  22. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jun 28, 2022
  23. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Jul 12, 2022
  24. ghost locked as resolved and limited conversation to collaborators on Aug 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.NetblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions