* Provide Upgrade response details
* fixing tests
* Address review feedback
* Save HttpStatusCode without CollectHttpResponseDetails
* Remove unnesessary skip on test
* Disable ConnectAsync_Failed on browser since CollectHttpResponseDetails is not supported
* Update src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs
Co-authored-by: Natalia Kondratyeva <knatalia@microsoft.com>
* Address review feedback
* Revert "Save HttpStatusCode without CollectHttpResponseDetails"
This reverts commit
0713bd8e292b6a76b0b9f297d95e466f11feff3b.
* renove using from ref
* Update test
* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/ClientWebSocket.cs
Co-authored-by: Natalia Kondratyeva <knatalia@microsoft.com>
* fixing Values and Enumerator for HttpResponseHeaders
* fixing Values and Enumerator for HttpResponseHeaders
* Apply suggestions from code review
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs
Co-authored-by: Stephen Toub <stoub@microsoft.com>
* Apply suggestions from code review
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
* Check CollectHttpResponseDetails setter
* disable CA1822 // Mark members as static
Co-authored-by: Natalia Kondratyeva <knatalia@microsoft.com>
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
// ------------------------------------------------------------------------------
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------
+
namespace System.Net.WebSockets
{
public sealed partial class ClientWebSocket : System.Net.WebSockets.WebSocket
public ClientWebSocket() { }
public override System.Net.WebSockets.WebSocketCloseStatus? CloseStatus { get { throw null; } }
public override string? CloseStatusDescription { get { throw null; } }
+ public System.Net.HttpStatusCode HttpStatusCode { get { throw null; } }
+ public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>>? HttpResponseHeaders { get { throw null; } set { } }
public System.Net.WebSockets.ClientWebSocketOptions Options { get { throw null; } }
public override System.Net.WebSockets.WebSocketState State { get { throw null; } }
public override string? SubProtocol { get { throw null; } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.CookieContainer? Cookies { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
+ public bool CollectHttpResponseDetails { get { throw null; } set { } }
+ [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.ICredentials? Credentials { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.TimeSpan KeepAliveInterval { get { throw null; } set { } }
<Compile Include="System\Net\WebSockets\ClientWebSocketOptions.cs" Condition="'$(TargetPlatformIdentifier)' != 'Browser'" />
<Compile Include="$(CommonPath)System\Net\UriScheme.cs" Link="Common\System\Net\UriScheme.cs" />
<Compile Include="$(CommonPath)System\Net\WebSockets\WebSocketValidate.cs" Link="Common\System\Net\WebSockets\WebSocketValidate.cs" />
+ <Compile Include="System\Net\WebSockets\HttpResponseHeadersReadOnlyCollection.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'Browser'">
<Compile Include="System\Net\WebSockets\WebSocketHandle.Managed.cs" />
set => throw new PlatformNotSupportedException();
}
+ [UnsupportedOSPlatform("browser")]
+ public bool CollectHttpResponseDetails
+ {
+ get => throw new PlatformNotSupportedException();
+ set => throw new PlatformNotSupportedException();
+ }
+
#endregion HTTP Settings
#region WebSocket Settings
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
}
}
+ public System.Net.HttpStatusCode HttpStatusCode => _innerWebSocket?.HttpStatusCode ?? 0;
+
+ // setter to clean up when not needed anymore
+ public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders
+ {
+ get => _innerWebSocket?.HttpResponseHeaders;
+ set
+ {
+ if (_innerWebSocket != null)
+ {
+ _innerWebSocket.HttpResponseHeaders = value;
+ }
+ }
+ }
+
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
{
return ConnectAsync(uri, null, cancellationToken);
internal List<string>? _requestedSubProtocols;
private Version _version = Net.HttpVersion.Version11;
private HttpVersionPolicy _versionPolicy = HttpVersionPolicy.RequestVersionOrLower;
+ private bool _collectHttpResponseDetails;
internal ClientWebSocketOptions() { } // prevent external instantiation
_buffer = buffer;
}
+ [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
+ public bool CollectHttpResponseDetails
+ {
+ get => _collectHttpResponseDetails;
+ set
+ {
+ ThrowIfReadOnly();
+ _collectHttpResponseDetails = value;
+ }
+ }
+
#endregion WebSocket settings
#region Helpers
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+
+namespace System.Net.WebSockets
+{
+ internal sealed class HttpResponseHeadersReadOnlyCollection : IReadOnlyDictionary<string, IEnumerable<string>>
+ {
+ private readonly HttpHeadersNonValidated _headers;
+
+ public HttpResponseHeadersReadOnlyCollection(HttpResponseHeaders headers) => _headers = headers.NonValidated;
+
+ public IEnumerable<string> this[string key] => _headers[key];
+
+ public IEnumerable<string> Keys
+ {
+ get
+ {
+ foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
+ {
+ yield return header.Key;
+ }
+ }
+ }
+
+ public IEnumerable<IEnumerable<string>> Values
+ {
+ get
+ {
+ foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
+ {
+ yield return header.Value;
+ }
+ }
+ }
+
+ public int Count => _headers.Count;
+
+ public bool ContainsKey(string key) => _headers.Contains(key);
+
+ public IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumerator()
+ {
+ foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
+ {
+ yield return new KeyValuePair<string, IEnumerable<string>>(header.Key, header.Value);
+ }
+ }
+
+ public bool TryGetValue(string key, [MaybeNullWhen(false)] out IEnumerable<string> value)
+ {
+ if (_headers.TryGetValues(key, out HeaderStringValues values))
+ {
+ value = values;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
internal sealed class WebSocketHandle
{
private WebSocketState _state = WebSocketState.Connecting;
+#pragma warning disable CA1822 // Mark members as static
+ public HttpStatusCode HttpStatusCode => (HttpStatusCode)0;
+#pragma warning restore CA1822 // Mark members as static
+
+ public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }
public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
+ public HttpStatusCode HttpStatusCode { get; private set; }
+
+ public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }
public static ClientWebSocketOptions CreateDefaultOptions() => new ClientWebSocketOptions() { Proxy = DefaultWebProxy.Instance };
invoker ??= new HttpMessageInvoker(SetupHandler(options, out disposeHandler));
HttpResponseMessage? response = null;
+ bool disposeResponse = false;
+
bool tryDowngrade = false;
try
{
}
Abort();
- response?.Dispose();
+ disposeResponse = true;
if (exc is WebSocketException ||
(exc is OperationCanceledException && cancellationToken.IsCancellationRequested))
}
finally
{
+ if (response is not null)
+ {
+ if (options.CollectHttpResponseDetails)
+ {
+ HttpStatusCode = response.StatusCode;
+ HttpResponseHeaders = new HttpResponseHeadersReadOnlyCollection(response.Headers);
+ }
+
+ if (disposeResponse)
+ {
+ response.Dispose();
+ }
+ }
+
// Disposing the handler will not affect any active stream wrapped in the WebSocket.
if (disposeHandler)
{
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net.Test.Common;
using System.Threading;
using System.Threading.Tasks;
catch (IOException) { }
}, new LoopbackServer.Options { WebSocketEndpoint = true });
}
+
+ [ConditionalFact(nameof(WebSocketsSupported))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
+ [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
+ public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure()
+ {
+ await LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ using (var clientWebSocket = new ClientWebSocket())
+ using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
+ {
+ clientWebSocket.Options.CollectHttpResponseDetails = true;
+ Task t = clientWebSocket.ConnectAsync(uri, cts.Token);
+ await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode);
+ Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
+ }
+ }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized), new LoopbackServer.Options { WebSocketEndpoint = true });
+ }
+
+ [ConditionalFact(nameof(WebSocketsSupported))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
+ [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
+ public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure_CustomHeader()
+ {
+ await LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ using (var clientWebSocket = new ClientWebSocket())
+ using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
+ {
+ clientWebSocket.Options.CollectHttpResponseDetails = true;
+ Task t = clientWebSocket.ConnectAsync(uri, cts.Token);
+ await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode);
+ Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
+ Assert.Contains("X-CustomHeader1", clientWebSocket.HttpResponseHeaders);
+ Assert.Contains("X-CustomHeader2", clientWebSocket.HttpResponseHeaders);
+ Assert.NotNull(clientWebSocket.HttpResponseHeaders.Values);
+ }
+ }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized, "X-CustomHeader1: Value1\r\nX-CustomHeader2: Value2\r\n"), new LoopbackServer.Options { WebSocketEndpoint = true });
+ }
+
+ [ConditionalFact(nameof(WebSocketsSupported))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
+ [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
+ public async Task ConnectAsync_HttpResponseDetailsCollectedOnSuccess_Extentions()
+ {
+ await LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ using (var clientWebSocket = new ClientWebSocket())
+ using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
+ {
+ clientWebSocket.Options.CollectHttpResponseDetails = true;
+ await clientWebSocket.ConnectAsync(uri, cts.Token);
+
+ Assert.Equal(HttpStatusCode.SwitchingProtocols, clientWebSocket.HttpStatusCode);
+ Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
+ Assert.Contains("Sec-WebSocket-Extensions", clientWebSocket.HttpResponseHeaders);
+ }
+ }, server => server.AcceptConnectionAsync(async connection =>
+ {
+ Dictionary<string, string> headers = await LoopbackHelper.WebSocketHandshakeAsync(connection, "X-CustomHeader1");
+ }), new LoopbackServer.Options { WebSocketEndpoint = true });
+ }
}
}