Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -28,6 +28,10 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
private readonly TimeSpan _timeout;
private int _lastStreamId;
private bool _expectClientDisconnect;
private bool _closeDeferred;
private int _lastRequestStreamId;
private int _lastGoAwayStreamId = int.MaxValue;
private int _closed;
private readonly SemaphoreSlim? _readLock;
private readonly SemaphoreSlim? _writeLock;

Expand All @@ -36,6 +40,8 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
public bool IsInvalid => _connectionSocket == null;
public Stream Stream => _connectionStream;
public Task<bool> SettingAckWaiter => _ignoredSettingsAckPromise?.Task;
internal bool DeferClose { get; set; }
internal bool IsCloseDeferred => _closeDeferred;

private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan timeout, Http2Options httpOptions)
{
Expand Down Expand Up @@ -179,9 +185,10 @@ public async Task SendConnectionPrefaceAsync()

public async Task WriteFrameAsync(Frame frame, CancellationToken cancellationToken = default)
{
Stream stream = _connectionStream ?? throw new ObjectDisposedException(nameof(Http2LoopbackConnection));
byte[] writeBuffer = new byte[Frame.FrameHeaderLength + frame.Length];
frame.WriteTo(writeBuffer);
await _connectionStream.WriteAsync(writeBuffer, 0, writeBuffer.Length, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, cancellationToken).ConfigureAwait(false);
}

public async Task WriteFramesAsync(Frame[] frames, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -247,6 +254,10 @@ public async Task<Frame> ReadFrameAsync(CancellationToken cancellationToken)
}

Frame header = Frame.ReadFrom(headerBytes);
if (header.Type == FrameType.Headers)
{
_lastRequestStreamId = Math.Max(_lastRequestStreamId, header.StreamId);
}

// Read the data segment of the frame, if it is present.
byte[] data = new byte[header.Length];
Expand Down Expand Up @@ -799,6 +810,7 @@ public async Task<SettingsFrame> ReadSettingsAsync()

public async Task SendGoAway(int lastStreamId, ProtocolErrors errorCode = ProtocolErrors.NO_ERROR)
{
_lastGoAwayStreamId = Math.Min(_lastGoAwayStreamId, lastStreamId);
GoAwayFrame frame = new GoAwayFrame(lastStreamId, (int)errorCode, new byte[] { }, 0);
await WriteFrameAsync(frame).ConfigureAwait(false);
}
Expand Down Expand Up @@ -932,13 +944,59 @@ public async Task SendResponseBodyAsync(int streamId, ReadOnlyMemory<byte> respo

public override async ValueTask DisposeAsync()
{
if (_closeDeferred)
{
return;
}

// Might have been already shutdown manually via WaitForConnectionShutdownAsync which nulls the _connectionStream.
if (_connectionStream != null)
{
if (DeferClose)
{
_closeDeferred = true;
try
{
await SendGoAway(Math.Min(_lastRequestStreamId, _lastGoAwayStreamId)).ConfigureAwait(false);
}
catch (ObjectDisposedException) when (Volatile.Read(ref _closed) != 0)
{
// The server can close connections while a failing test is still unwinding.
}
catch (IOException)
{
// The client may already have closed the connection.
}
catch (SocketException)
{
// The client may already have closed the connection.
}
return;
}

await ShutdownIgnoringErrorsAsync(_lastStreamId);
}
}

internal void Close()
{
if (Interlocked.Exchange(ref _closed, 1) != 0)
{
return;
}

try
{
_connectionStream?.Dispose();
}
finally
{
_connectionSocket?.Dispose();
_connectionStream = null;
_connectionSocket = null;
Comment thread
rzikm marked this conversation as resolved.
}
}

//
// GenericLoopbackServer implementation
//
Expand Down Expand Up @@ -1047,7 +1105,14 @@ public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode st
await SendResponseBodyAsync(streamId, Encoding.ASCII.GetBytes(content)).ConfigureAwait(false);
}

await WaitForConnectionShutdownAsync().ConfigureAwait(false);
if (DeferClose)
{
await DisposeAsync().ConfigureAwait(false);
}
else
{
await WaitForConnectionShutdownAsync().ConfigureAwait(false);
}

return requestData;
}
Expand Down
55 changes: 46 additions & 9 deletions src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,36 @@ public Task<Http2LoopbackConnection> AcceptConnectionAsync()

public async Task<Http2LoopbackConnection> AcceptConnectionAsync(TimeSpan? timeout)
{
RemoveInvalidConnections();

if (!AllowMultipleConnections && _connections.Count != 0)
Socket listenSocket;
lock (_connections)
{
throw new InvalidOperationException("Connection already established. Set `AllowMultipleConnections = true` to bypass.");
listenSocket = _listenSocket ?? throw new ObjectDisposedException(nameof(Http2LoopbackServer));
RemoveInvalidConnections();

if (!AllowMultipleConnections && _connections.Exists(c => !c.IsCloseDeferred))
{
throw new InvalidOperationException("Connection already established. Set `AllowMultipleConnections = true` to bypass.");
}
}

Socket connectionSocket = await _listenSocket.AcceptAsync().ConfigureAwait(false);
Socket connectionSocket = await listenSocket.AcceptAsync().ConfigureAwait(false);

var stream = new NetworkStream(connectionSocket, ownsSocket: true);
var wrapper = new SocketWrapper(connectionSocket);
Http2LoopbackConnection connection =
timeout != null ? await Http2LoopbackConnection.CreateAsync(wrapper, stream, _options, timeout.Value).ConfigureAwait(false) :
await Http2LoopbackConnection.CreateAsync(wrapper, stream, _options).ConfigureAwait(false);
_connections.Add(connection);
lock (_connections)
{
if (_listenSocket is null)
{
connection.Close();
throw new ObjectDisposedException(nameof(Http2LoopbackServer));
}

connection.DeferClose = _options.DeferConnectionClose;
_connections.Add(connection);
}

return connection;
}
Expand Down Expand Up @@ -135,10 +150,22 @@ public async Task<Http2LoopbackConnection> EstablishConnectionAsync(TimeSpan? ti

public override void Dispose()
{
if (_listenSocket != null)
lock (_connections)
{
_listenSocket.Dispose();
_listenSocket = null;
if (_listenSocket != null)
{
_listenSocket.Dispose();
_listenSocket = null;
}

if (_options.DeferConnectionClose)
{
foreach (Http2LoopbackConnection connection in _connections)
{
connection.Close();
}
_connections.Clear();
}
}
}

Expand Down Expand Up @@ -187,6 +214,16 @@ public class Http2Options : GenericLoopbackOptions
public bool EnableTransparentPingResponse { get; set; } = true;
public bool EnsureThreadSafeIO { get; set; }

// Transfer disposed connections to the server until its scope ends, after the client has consumed
// its responses. WinHTTP can discard buffered data when the server sends FIN too early.
// Tests requiring immediate closure can opt out or use explicit connection shutdown methods.
public bool DeferConnectionClose { get; set; } =
#if WINHTTPHANDLER_TEST
true;
#else
false;
#endif

public Http2Options()
{
SslProtocols = SslProtocols.Tls12;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ public PlatformHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper o
}

#if NET
[ActiveIssue("https://github.com/dotnet/runtime/issues/126867", typeof(PlatformDetection), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.IsX64Process))]
[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows10Version1607OrGreater))]
public sealed class PlatformHandlerTest_Cookies_Http2 : HttpClientHandlerTest_Cookies
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Net.Http.Functional.Tests;
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -34,6 +35,105 @@ public WinHttpHandlerTest(ITestOutputHelper output)
_output = output;
}

#if !NETFRAMEWORK
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows10Version1607OrGreater))]
[InlineData(false)]
[InlineData(true)]
public async Task GetAsync_Http2LoopbackConnectionDisposed_ResponseRemainsReadable(bool handleRequest)
{
const string Content = "Response read after the server finishes sending";
var serverFinished = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Http2LoopbackConnection connection = null;

await Http2LoopbackServer.CreateClientAndServerAsync(async address =>
{
using var client = new HttpClient(new WinHttpHandler
{
ServerCertificateValidationCallback = TestHelper.AllowAllCertificates
});
using var request = new HttpRequestMessage(HttpMethod.Get, address) { Version = HttpVersion20.Value };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
await serverFinished.Task.WaitAsync(TestHelper.PassingTestTimeout);

Assert.False(connection.IsInvalid);
Assert.Equal(Content, await response.Content.ReadAsStringAsync());
},
async server =>
{
try
{
connection = await server.EstablishConnectionAsync();
await using (connection)
{
if (handleRequest)
{
await connection.HandleRequestAsync(content: Content);
}
else
{
int streamId = await connection.ReadRequestHeaderAsync();
await connection.SendResponseHeadersAsync(streamId, endStream: false);
await connection.SendResponseBodyAsync(streamId, Encoding.ASCII.GetBytes(Content));
}
}

serverFinished.SetResult(true);
}
catch (Exception e)
{
serverFinished.TrySetException(e);
throw;
}
});

Assert.True(connection.IsInvalid);
}

[Fact]
public async Task Http2LoopbackConnection_CloseDuringDeferredDispose_Completes()
{
var writeStarted = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var finishWrite = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
bool closed = false;
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
using var stream = new DelegateStream(
canReadFunc: () => true,
canWriteFunc: () => true,
readAsyncFunc: (buffer, offset, count, token) =>
{
byte[] preface = Encoding.ASCII.GetBytes(Http2LoopbackConnection.Http2Prefix);
Assert.Equal(preface.Length, count);
preface.CopyTo(buffer, offset);
return Task.FromResult(preface.Length);
},
writeAsyncFunc: async (buffer, offset, count, token) =>
{
writeStarted.TrySetResult(true);
await finishWrite.Task.WaitAsync(TestHelper.PassingTestTimeout);
Assert.True(closed);
throw new ObjectDisposedException(nameof(DelegateStream));
},
disposeFunc: _ => closed = true);

Http2LoopbackConnection connection = await Http2LoopbackConnection.CreateAsync(
new SocketWrapper(socket), stream, new Http2Options { UseSsl = false });
connection.DeferClose = true;
Task disposeTask = connection.DisposeAsync().AsTask();
try
{
await writeStarted.Task.WaitAsync(TestHelper.PassingTestTimeout);
connection.Close();
}
finally
{
finishWrite.TrySetResult(true);
}

await disposeTask.WaitAsync(TestHelper.PassingTestTimeout);
Assert.True(connection.IsInvalid);
}
#endif

[OuterLoop]
[Fact]
public void SendAsync_SimpleGet_Success()
Expand Down
Loading