[API Proposal] ClientWebSocket upgrade response details  #25918

Description

@amrmahdi

Updated by @CarnaViire

Background and motivation

ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

API Proposal

There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

Option 1. ConnectAsync overload with a result object to fill in

// NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

Usage:

ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

Pros:

  • Provides data that is essentially a connect result, from an overload of ConnectAsync method
  • User can reuse the result objects
  • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

Cons:

  • Need to manually create additional object (no out var syntax here)
  • User needs to ensure thread-safety, especially if reusing result objects

Option 2. WebSocket property with opt-in setting

// EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

Usage:

ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

Pros:

  • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
  • No additional objects

Cons:

  • Expanding ClientWebSocket object leads to increased memory footprint
  • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

Other alternatives considered, but rejected:

  • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
  • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
  • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
  • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

Original post by @amrmahdi

Related to #19405

ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

Repro code

varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

Behavior on .NET 462

Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
--- End of inner exception stack trace ---
at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at ConsoleApp1.Program.Main(String[] args)

Behavior on .NET Core 2.1 preview 2

 Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
at ConsoleApp1.Program.Main(String[] args)

As you can see on .NET 462, the inner exception is a WebException with the error details.

Proposed Fix

Create an inner exception of type WebException in a similar fashion to
https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
and throw if the response is not 200.

The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [API Proposal] ClientWebSocket upgrade response details  #25918

    Description

    @amrmahdi

    Updated by @CarnaViire

    Background and motivation

    ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

    In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

    The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

    There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

    API Proposal

    There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

    Option 1. ConnectAsync overload with a result object to fill in

    // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

    Usage:

    ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

    Pros:

    • Provides data that is essentially a connect result, from an overload of ConnectAsync method
    • User can reuse the result objects
    • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

    Cons:

    • Need to manually create additional object (no out var syntax here)
    • User needs to ensure thread-safety, especially if reusing result objects

    Option 2. WebSocket property with opt-in setting

    // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

    Usage:

    ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

    Pros:

    • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
    • No additional objects

    Cons:

    • Expanding ClientWebSocket object leads to increased memory footprint
    • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

    Other alternatives considered, but rejected:

    • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
    • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
    • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
    • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

    Original post by @amrmahdi

    Related to #19405

    ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

    Repro code

    varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

    Behavior on .NET 462

    Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
    at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
    at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
    --- End of stack trace from previous location where exception was thrown ---
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
    at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
    --- End of inner exception stack trace ---
    at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
    at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
    at ConsoleApp1.Program.Main(String[] args)
    

    Behavior on .NET Core 2.1 preview 2

     Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
    at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
    at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
    at ConsoleApp1.Program.Main(String[] args)
    

    As you can see on .NET 462, the inner exception is a WebException with the error details.

    Proposed Fix

    Create an inner exception of type WebException in a similar fashion to
    https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
    and throw if the response is not 200.

    The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

    Metadata

    Metadata

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [API Proposal] ClientWebSocket upgrade response details  #25918

      Description

      @amrmahdi

      Updated by @CarnaViire

      Background and motivation

      ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

      In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

      The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

      There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

      API Proposal

      There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

      Option 1. ConnectAsync overload with a result object to fill in

      // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

      Usage:

      ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

      Pros:

      • Provides data that is essentially a connect result, from an overload of ConnectAsync method
      • User can reuse the result objects
      • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

      Cons:

      • Need to manually create additional object (no out var syntax here)
      • User needs to ensure thread-safety, especially if reusing result objects

      Option 2. WebSocket property with opt-in setting

      // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

      Usage:

      ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

      Pros:

      • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
      • No additional objects

      Cons:

      • Expanding ClientWebSocket object leads to increased memory footprint
      • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

      Other alternatives considered, but rejected:

      • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
      • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
      • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
      • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

      Original post by @amrmahdi

      Related to #19405

      ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

      Repro code

      varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

      Behavior on .NET 462

      Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
      at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
      at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
      --- End of stack trace from previous location where exception was thrown ---
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
      at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
      --- End of inner exception stack trace ---
      at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
      --- End of stack trace from previous location where exception was thrown ---
      at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
      at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
      at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
      at ConsoleApp1.Program.Main(String[] args)
      

      Behavior on .NET Core 2.1 preview 2

       Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
      at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
      at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
      at ConsoleApp1.Program.Main(String[] args)
      

      As you can see on .NET 462, the inner exception is a WebException with the error details.

      Proposed Fix

      Create an inner exception of type WebException in a similar fashion to
      https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
      and throw if the response is not 200.

      The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

      Metadata

      Metadata

      Labels

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [API Proposal] ClientWebSocket upgrade response details  #25918

        Description

        @amrmahdi

        Updated by @CarnaViire

        Background and motivation

        ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

        In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

        The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

        There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

        API Proposal

        There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

        Option 1. ConnectAsync overload with a result object to fill in

        // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

        Usage:

        ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

        Pros:

        • Provides data that is essentially a connect result, from an overload of ConnectAsync method
        • User can reuse the result objects
        • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

        Cons:

        • Need to manually create additional object (no out var syntax here)
        • User needs to ensure thread-safety, especially if reusing result objects

        Option 2. WebSocket property with opt-in setting

        // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

        Usage:

        ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

        Pros:

        • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
        • No additional objects

        Cons:

        • Expanding ClientWebSocket object leads to increased memory footprint
        • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

        Other alternatives considered, but rejected:

        • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
        • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
        • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
        • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

        Original post by @amrmahdi

        Related to #19405

        ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

        Repro code

        varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

        Behavior on .NET 462

        Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
        at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
        at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
        --- End of stack trace from previous location where exception was thrown ---
        at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
        at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
        --- End of inner exception stack trace ---
        at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
        --- End of stack trace from previous location where exception was thrown ---
        at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
        at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
        at ConsoleApp1.Program.Main(String[] args)
        

        Behavior on .NET Core 2.1 preview 2

         Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
        at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
        at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
        at ConsoleApp1.Program.Main(String[] args)
        

        As you can see on .NET 462, the inner exception is a WebException with the error details.

        Proposed Fix

        Create an inner exception of type WebException in a similar fashion to
        https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
        and throw if the response is not 200.

        The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

        Metadata

        Metadata

        Labels

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [API Proposal] ClientWebSocket upgrade response details  #25918

          Description

          @amrmahdi

          Updated by @CarnaViire

          Background and motivation

          ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

          In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

          The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

          There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

          API Proposal

          There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

          Option 1. ConnectAsync overload with a result object to fill in

          // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

          Usage:

          ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

          Pros:

          • Provides data that is essentially a connect result, from an overload of ConnectAsync method
          • User can reuse the result objects
          • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

          Cons:

          • Need to manually create additional object (no out var syntax here)
          • User needs to ensure thread-safety, especially if reusing result objects

          Option 2. WebSocket property with opt-in setting

          // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

          Usage:

          ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

          Pros:

          • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
          • No additional objects

          Cons:

          • Expanding ClientWebSocket object leads to increased memory footprint
          • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

          Other alternatives considered, but rejected:

          • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
          • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
          • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
          • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

          Original post by @amrmahdi

          Related to #19405

          ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

          Repro code

          varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

          Behavior on .NET 462

          Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
          at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
          at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
          --- End of stack trace from previous location where exception was thrown ---
          at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
          at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
          at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
          --- End of inner exception stack trace ---
          at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
          --- End of stack trace from previous location where exception was thrown ---
          at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
          at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
          at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
          at ConsoleApp1.Program.Main(String[] args)
          

          Behavior on .NET Core 2.1 preview 2

           Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
          at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
          at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
          at ConsoleApp1.Program.Main(String[] args)
          

          As you can see on .NET 462, the inner exception is a WebException with the error details.

          Proposed Fix

          Create an inner exception of type WebException in a similar fashion to
          https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
          and throw if the response is not 200.

          The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

          Metadata

          Metadata

          Labels

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [API Proposal] ClientWebSocket upgrade response details  #25918

            Description

            @amrmahdi

            Updated by @CarnaViire

            Background and motivation

            ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

            In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

            The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

            There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

            API Proposal

            There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

            Option 1. ConnectAsync overload with a result object to fill in

            // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

            Usage:

            ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

            Pros:

            • Provides data that is essentially a connect result, from an overload of ConnectAsync method
            • User can reuse the result objects
            • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

            Cons:

            • Need to manually create additional object (no out var syntax here)
            • User needs to ensure thread-safety, especially if reusing result objects

            Option 2. WebSocket property with opt-in setting

            // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

            Usage:

            ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

            Pros:

            • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
            • No additional objects

            Cons:

            • Expanding ClientWebSocket object leads to increased memory footprint
            • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

            Other alternatives considered, but rejected:

            • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
            • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
            • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
            • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

            Original post by @amrmahdi

            Related to #19405

            ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

            Repro code

            varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

            Behavior on .NET 462

            Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
            at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
            at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
            --- End of stack trace from previous location where exception was thrown ---
            at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
            at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
            at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
            --- End of inner exception stack trace ---
            at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
            --- End of stack trace from previous location where exception was thrown ---
            at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
            at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
            at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
            at ConsoleApp1.Program.Main(String[] args)
            

            Behavior on .NET Core 2.1 preview 2

             Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
            at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
            at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
            at ConsoleApp1.Program.Main(String[] args)
            

            As you can see on .NET 462, the inner exception is a WebException with the error details.

            Proposed Fix

            Create an inner exception of type WebException in a similar fashion to
            https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
            and throw if the response is not 200.

            The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

            Metadata

            Metadata

            Labels

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal] ClientWebSocket upgrade response details  #25918

              Description

              @amrmahdi

              Updated by @CarnaViire

              Background and motivation

              ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

              In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

              The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

              There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

              API Proposal

              There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

              Option 1. ConnectAsync overload with a result object to fill in

              // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

              Usage:

              ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

              Pros:

              • Provides data that is essentially a connect result, from an overload of ConnectAsync method
              • User can reuse the result objects
              • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

              Cons:

              • Need to manually create additional object (no out var syntax here)
              • User needs to ensure thread-safety, especially if reusing result objects

              Option 2. WebSocket property with opt-in setting

              // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

              Usage:

              ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

              Pros:

              • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
              • No additional objects

              Cons:

              • Expanding ClientWebSocket object leads to increased memory footprint
              • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

              Other alternatives considered, but rejected:

              • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
              • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
              • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
              • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

              Original post by @amrmahdi

              Related to #19405

              ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

              Repro code

              varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

              Behavior on .NET 462

              Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
              at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
              at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
              --- End of stack trace from previous location where exception was thrown ---
              at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
              at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
              at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
              --- End of inner exception stack trace ---
              at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
              --- End of stack trace from previous location where exception was thrown ---
              at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
              at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
              at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
              at ConsoleApp1.Program.Main(String[] args)
              

              Behavior on .NET Core 2.1 preview 2

               Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
              at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
              at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
              at ConsoleApp1.Program.Main(String[] args)
              

              As you can see on .NET 462, the inner exception is a WebException with the error details.

              Proposed Fix

              Create an inner exception of type WebException in a similar fashion to
              https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
              and throw if the response is not 200.

              The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

              Metadata

              Metadata

              Labels

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [API Proposal] ClientWebSocket upgrade response details  #25918

                Description

                @amrmahdi

                Updated by @CarnaViire

                Background and motivation

                ClientWebSocket currently doesn't provide any details about upgrade response. However, the information about response headers and status code might be important in both failure and success scenarios.

                In case of failure, the status code can help to distinguish between retriable and non-retriable errors (server doesn't support web sockets at all vs just a small transient error). Headers might also contain additional information on how to handle the situation.

                The headers are also useful even in case of a success web socket connect, e.g. they can contain a token tied to a session, or some info related to subprotocol version, or that the server can go down soon, etc.

                There are three asks on GH for this ATM with a total of 21 distinct upvotes#28331, #62474 and #25918 (this issue).

                API Proposal

                There are two alternatives, that are usable regardless of success/failure scenario, and also both opt-in, in order not to regress the existing usages in size and perf.

                Option 1. ConnectAsync overload with a result object to fill in

                // NEWclassWebSocketConnectResult{publicint?HttpStatusCode{get;set;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}}// EXISTINGclassClientWebSocket{// EXISTINGpublicTaskConnectAsync(Uriuri,CancellationTokencancellationToken);// NEWpublicTaskConnectAsync(Uriuri,WebSocketConnectResultresult,CancellationTokencancellationToken);}

                Usage:

                ClientWebSocketws=new();WebSocketConnectResultresult=new();try{awaitws.ConnectAsync(uri,result,default);// success scenarioProcessSuccess(result.HttpResponseHeaders);}catch(WebSocketException){// failure scenarioif(connectResult.HttpStatusCode!=null){ProcessFailure(result.HttpStatusCode,result.HttpResponseHeaders);}}

                Pros:

                • Provides data that is essentially a connect result, from an overload of ConnectAsync method
                • User can reuse the result objects
                • Result object is independent from the ClientWebSocket object and its ownership/lifetime is "naturally" in hands of the user

                Cons:

                • Need to manually create additional object (no out var syntax here)
                • User needs to ensure thread-safety, especially if reusing result objects

                Option 2. WebSocket property with opt-in setting

                // EXISTINGclassClientWebSocketOptions{// NEWpublicboolCollectHttpResponseDetails{get;set;}=false;}// EXISTINGclassClientWebSocket{// EXISTINGpublicstring?SubProtocol{get;}// NEWpublicint?HttpStatusCode{get;}publicIReadOnlyDictionary<string,IEnumerable<string>>?HttpResponseHeaders{get;set;}// setter to clean up when not needed anymore}

                Usage:

                ClientWebSocketws=new();ws.Options.CollectHttpResponseDetails=true;try{awaitws.ConnectAsync(uri,default);// success scenarioProcessSuccess(ws.HttpResponseHeaders);ws.HttpResponseHeaders=null;// clean up (if needed)}catch(WebSocketException){// failure scenarioif(ws.HttpStatusCode!=null){ProcessFailure(ws.HttpStatusCode,ws.HttpResponseHeaders);}}

                Pros:

                • SubProtocol, which also can be treated as "connect result", is already a part of ClientWebSocket object
                • No additional objects

                Cons:

                • Expanding ClientWebSocket object leads to increased memory footprint
                • Even though it is possible to clean up by setting headers to null, user needs to be aware of that approach.

                Other alternatives considered, but rejected:

                • Returning result object from the method itself (Task<WebSocketConnectResult>) would either only work in success scenario or will require unconventional handling of every possible exception by storing it into the result object
                • Having different approaches for success and failure scenarios doubles the API changes needed and is harder to use, plus adding a new derived exception is bad for discoverability.
                • Using full HttpResponseMessage is dangerous, it is easy to misuse (and end up breaking the protocol/aborting connection by disposing/etc)
                • In general, we want to distance from System.Net types in favor of plain types like int and IDictionary to enable wider usage.

                Original post by @amrmahdi

                Related to #19405

                ClientWebSocket on .NET Core does not provide the upgrade request errors in the exception details as it does on the .NET Framework.

                Repro code

                varclient=newClientWebSocket();client.ConnectAsync(newUri("wss://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1"),CancellationToken.None).GetAwaiter().GetResult();

                Behavior on .NET 462

                Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server ---> System.Net.WebException: The remote server returned an error: (403) Forbidden.
                at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
                at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
                --- End of stack trace from previous location where exception was thrown ---
                at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
                at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
                at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
                --- End of inner exception stack trace ---
                at System.Net.WebSockets.ClientWebSocket.<ConnectAsyncCore>d__21.MoveNext()
                --- End of stack trace from previous location where exception was thrown ---
                at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
                at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
                at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
                at ConsoleApp1.Program.Main(String[] args)
                

                Behavior on .NET Core 2.1 preview 2

                 Unhandled Exception: System.Net.WebSockets.WebSocketException: Unable to connect to the remote server
                at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
                at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
                at ConsoleApp1.Program.Main(String[] args)
                

                As you can see on .NET 462, the inner exception is a WebException with the error details.

                Proposed Fix

                Create an inner exception of type WebException in a similar fashion to
                https://github.com/dotnet/corefx/blob/6acd74dda7bc4f585d2c4006da4a8b2deb0261ad/src/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1211
                and throw if the response is not 200.

                The original WebException in .NET framework was thrown fromHttpWebRequest.GetResponseAsync(), so I think the exception needs to be bubbled up in a similar way.

                Metadata

                Metadata

                Labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions