Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide upgrade response details #71757

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
// 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; } }
Expand All @@ -32,6 +35,8 @@ internal ClientWebSocketOptions() { }
[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 { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ public System.Net.CookieContainer Cookies
set => throw new PlatformNotSupportedException();
}

[UnsupportedOSPlatform("browser")]
public bool CollectHttpResponseDetails
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

#endregion HTTP Settings

#region WebSocket Settings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -50,6 +51,21 @@ public override WebSocketState State
}
}

public System.Net.HttpStatusCode HttpStatusCode => _innerWebSocket != null ? _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)
{
ArgumentNullException.ThrowIfNull(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<by
_buffer = buffer;
}

[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public bool CollectHttpResponseDetails { get; set; }

#endregion WebSocket settings

#region Helpers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 IReadOnlyDictionary<string, HeaderStringValues> _headers;

public HttpResponseHeadersReadOnlyCollection(HttpResponseHeaders headers) => _headers = headers.NonValidated;

public IEnumerable<string> this[string key] => _headers[key];

public IEnumerable<string> Keys => _headers.Keys;

public IEnumerable<IEnumerable<string>> Values => (IEnumerable<IEnumerable<string>>)_headers.Values;

public int Count => _headers.Count;

public bool ContainsKey(string key) => _headers.ContainsKey(key);
public IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumerator() => (IEnumerator<KeyValuePair<string, IEnumerable<string>>>)_headers.GetEnumerator();
public bool TryGetValue(string key, [MaybeNullWhen(false)] out IEnumerable<string> value)
{
bool res = _headers.TryGetValue(key, out HeaderStringValues headerStringValues);
value = headerStringValues;
return res;
}

IEnumerator IEnumerable.GetEnumerator() => _headers.GetEnumerator();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Threading;
using System.Threading.Tasks;

Expand All @@ -9,6 +10,9 @@ namespace System.Net.WebSockets
internal sealed class WebSocketHandle
{
private WebSocketState _state = WebSocketState.Connecting;
public HttpStatusCode HttpStatusCode { get; set; }

public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }

public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ internal sealed class WebSocketHandle

public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
public HttpStatusCode HttpStatusCode { get; set; }

public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }

public static ClientWebSocketOptions CreateDefaultOptions() => new ClientWebSocketOptions() { Proxy = DefaultWebProxy.Instance };

Expand All @@ -47,6 +50,7 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
HttpResponseMessage? response = null;
SocketsHttpHandler? handler = null;
bool disposeHandler = true;
bool disposeResponse = false;
try
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
Expand Down Expand Up @@ -226,7 +230,7 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
}

Abort();
response?.Dispose();
disposeResponse = true;

if (exc is WebSocketException ||
(exc is OperationCanceledException && cancellationToken.IsCancellationRequested))
Expand All @@ -238,6 +242,17 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
}
finally
{
if (options.CollectHttpResponseDetails && response != null)
{
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)
{
Expand Down
41 changes: 41 additions & 0 deletions src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,46 @@ await server.AcceptConnectionAsync(async connection =>
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_HttpResponseDetailsCollectedOnSuccess_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.SwitchingProtocols, clientWebSocket.HttpStatusCode);
Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
Assert.Contains("X-CustomHeader1", clientWebSocket.HttpResponseHeaders);
}
}, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.SwitchingProtocols, "X-CustomHeader1:Value1"), new LoopbackServer.Options { WebSocketEndpoint = true });
}
}
}