From ba0fc008b3b2bd90067ac5157a45547e28a2fb0b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 May 2025 18:10:22 +0000
Subject: [PATCH 1/5] Initial plan for issue
From 94b010698924f5947ab82f35ff1267f8aa613617 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 May 2025 18:17:37 +0000
Subject: [PATCH 2/5] Fix HTTP/2 pings with zero connection lifetime
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../HttpConnectionPoolManager.cs | 5 +-
...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 197 ++++++++++++++++++
2 files changed, 201 insertions(+), 1 deletion(-)
create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
index 27181c7b5b8a95..8424d1b26cebe2 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
@@ -69,10 +69,13 @@ public HttpConnectionPoolManager(HttpConnectionSettings settings)
// However, we can only do such optimizations if we're not also tracking
// connections per server, as we use data in the associated data structures
// to do that tracking.
+ // Additionally, we should not avoid storing connections if keep-alive ping is configured,
+ // as the heartbeat timer is needed for ping functionality.
bool avoidStoringConnections =
settings._maxConnectionsPerServer == int.MaxValue &&
(settings._pooledConnectionIdleTimeout == TimeSpan.Zero ||
- settings._pooledConnectionLifetime == TimeSpan.Zero);
+ settings._pooledConnectionLifetime == TimeSpan.Zero) &&
+ settings._keepAlivePingDelay == Timeout.InfiniteTimeSpan;
// Start out with the timer not running, since we have no pools.
// When it does run, run it with a frequency based on the idle timeout.
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
new file mode 100644
index 00000000000000..09c5c3702a3bab
--- /dev/null
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
@@ -0,0 +1,197 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Test.Common;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Http.Functional.Tests
+{
+ [Collection(nameof(DisableParallelization))]
+ [ConditionalClass(typeof(SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test), nameof(IsSupported))]
+ public sealed class SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test : HttpClientHandlerTestBase
+ {
+ public static readonly bool IsSupported = PlatformDetection.SupportsAlpn && PlatformDetection.IsNotBrowser;
+
+ protected override Version UseVersion => HttpVersion20.Value;
+
+ private int _pingCounter;
+ private Http2LoopbackConnection _connection;
+ private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1);
+ private Channel _framesChannel = Channel.CreateUnbounded();
+ private CancellationTokenSource _incomingFramesCts = new CancellationTokenSource();
+ private Task _incomingFramesTask;
+ private TaskCompletionSource _serverFinished = new TaskCompletionSource();
+ private bool _sendPingResponse = true;
+
+ private static Http2Options NoAutoPingResponseHttp2Options => new Http2Options() { EnableTransparentPingResponse = false };
+
+ private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
+
+ public SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [OuterLoop("Runs long")]
+ [Fact]
+ public async Task KeepAlivePing_ZeroConnectionLifetime_PingsStillWork()
+ {
+ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
+ handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
+ handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
+ handler.KeepAlivePingDelay = TimeSpan.FromSeconds(1);
+ handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime should still allow pings
+
+ using HttpClient client = new HttpClient(handler);
+ client.DefaultRequestVersion = HttpVersion.Version20;
+
+ // Warmup request to create connection:
+ HttpResponseMessage response0 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
+
+ // Actual request:
+ HttpResponseMessage response1 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
+
+ // Let connection live until server finishes:
+ await _serverFinished.Task.WaitAsync(TestTimeout);
+ },
+ async server =>
+ {
+ await EstablishConnectionAsync(server);
+
+ // Warmup the connection.
+ int streamId1 = await ReadRequestHeaderAsync();
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+ // Request under the test scope.
+ int streamId2 = await ReadRequestHeaderAsync();
+ Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+
+ // Simulate inactive period:
+ await Task.Delay(5_000);
+
+ // Even with zero lifetime, we should still receive pings for active streams:
+ // We may receive one RTT PING in response to HEADERS.
+ // Upon that, we expect to receive at least 1 keep alive PING:
+ Assert.True(_pingCounter > 1);
+
+ // Finish the response:
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+ await TerminateLoopbackConnectionAsync();
+
+ List unexpectedFrames = new List();
+ while (_framesChannel.Reader.Count > 0)
+ {
+ Frame unexpectedFrame = await _framesChannel.Reader.ReadAsync();
+ unexpectedFrames.Add(unexpectedFrame);
+ }
+
+ Assert.False(unexpectedFrames.Any(), "Received unexpected frames: \n" + string.Join('\n', unexpectedFrames.Select(f => f.ToString()).ToArray()));
+ }, NoAutoPingResponseHttp2Options);
+ }
+
+ private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Frame frame = await _connection.ReadFrameAsync(cancellationToken);
+
+ if (frame is null)
+ {
+ break;
+ }
+
+ if (frame is PingFrame pingFrame)
+ {
+ if (pingFrame.AckFlag)
+ {
+ _output?.WriteLine($"Received unexpected PING ACK ({pingFrame.Data})");
+ await _framesChannel.Writer.WriteAsync(frame, cancellationToken);
+ }
+ else
+ {
+ _output?.WriteLine($"Received PING ({pingFrame.Data})");
+ Interlocked.Increment(ref _pingCounter);
+
+ if (_sendPingResponse)
+ {
+ await GuardConnectionWriteAsync(() => _connection.SendPingAckAsync(pingFrame.Data, cancellationToken), cancellationToken);
+ }
+ }
+ }
+ else if (frame is WindowUpdateFrame windowUpdateFrame)
+ {
+ _output?.WriteLine($"Received WINDOW_UPDATE");
+ }
+ else
+ {
+ //_output?.WriteLine($"Received {frame}");
+ await _framesChannel.Writer.WriteAsync(frame, cancellationToken);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+
+ _output?.WriteLine("ProcessIncomingFramesAsync finished");
+ await _connection.DisposeAsync();
+ }
+
+ private async Task EstablishConnectionAsync(Http2LoopbackServer server)
+ {
+ _connection = await server.EstablishConnectionAsync();
+ _incomingFramesTask = ProcessIncomingFramesAsync(_incomingFramesCts.Token);
+ }
+
+ private async Task TerminateLoopbackConnectionAsync()
+ {
+ _serverFinished.SetResult();
+ _incomingFramesCts.Cancel();
+ await _incomingFramesTask;
+ }
+
+ private async Task GuardConnectionWriteAsync(Func action, CancellationToken cancellationToken = default)
+ {
+ await _writeSemaphore.WaitAsync(cancellationToken);
+ await action();
+ _writeSemaphore.Release();
+ }
+
+ private async Task ReadRequestHeaderFrameAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default)
+ {
+ // Receive HEADERS frame for request.
+ Frame frame = await _framesChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
+ if (frame == null)
+ {
+ throw new IOException("Failed to read Headers frame.");
+ }
+
+ Assert.Equal(FrameType.Headers, frame.Type);
+ Assert.Equal(FrameFlags.EndHeaders, frame.Flags & FrameFlags.EndHeaders);
+ if (expectEndOfStream)
+ {
+ Assert.Equal(FrameFlags.EndStream, frame.Flags & FrameFlags.EndStream);
+ }
+ return (HeadersFrame)frame;
+ }
+
+ private async Task ReadRequestHeaderAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default)
+ {
+ HeadersFrame frame = await ReadRequestHeaderFrameAsync(expectEndOfStream, cancellationToken);
+ return frame.StreamId;
+ }
+ }
+}
\ No newline at end of file
From 2c238adc494e6b0c33917a2861f2477fbbb84e88 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 May 2025 18:19:09 +0000
Subject: [PATCH 3/5] Add additional test case for zero lifetime without ping
config
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
index 09c5c3702a3bab..d698bcfd7b0048 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
@@ -100,6 +100,56 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
}, NoAutoPingResponseHttp2Options);
}
+ [OuterLoop("Runs long")]
+ [Fact]
+ public async Task KeepAlivePing_ZeroConnectionLifetime_NoPingConfig_NoPingsSent()
+ {
+ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
+ // Don't configure KeepAlivePing settings
+ handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime
+
+ using HttpClient client = new HttpClient(handler);
+ client.DefaultRequestVersion = HttpVersion.Version20;
+
+ // Warmup request to create connection:
+ HttpResponseMessage response0 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
+
+ // Actual request:
+ HttpResponseMessage response1 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
+
+ // Let connection live until server finishes:
+ await _serverFinished.Task.WaitAsync(TestTimeout);
+ },
+ async server =>
+ {
+ await EstablishConnectionAsync(server);
+
+ // Warmup the connection.
+ int streamId1 = await ReadRequestHeaderAsync();
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+ // Request under the test scope.
+ int streamId2 = await ReadRequestHeaderAsync();
+ Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+
+ // Simulate inactive period:
+ await Task.Delay(5_000);
+
+ // With zero lifetime and no ping config, we should not receive keep alive pings
+ // We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
+ Assert.True(_pingCounter <= 1);
+
+ // Finish the response:
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+ await TerminateLoopbackConnectionAsync();
+ }, NoAutoPingResponseHttp2Options);
+ }
+
private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
{
try
From f1088d8fc56de51d6869696d02d573d67453465d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 May 2025 18:26:44 +0000
Subject: [PATCH 4/5] Add new test file to csproj for test inclusion
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../FunctionalTests/System.Net.Http.Functional.Tests.csproj | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
index 340819fc688edb..74811f21bc9926 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
@@ -179,6 +179,7 @@
+
From 2fc0eb778564bc51229f25a0a36513812ae69d47 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Jun 2025 18:49:59 +0000
Subject: [PATCH 5/5] Consolidate zero lifetime ping tests into existing test
file
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
...cketsHttpHandlerTest.Http2KeepAlivePing.cs | 112 ++++++++
...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 247 ------------------
.../System.Net.Http.Functional.Tests.csproj | 1 -
3 files changed, 112 insertions(+), 248 deletions(-)
delete mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs
index 0f1c1b8d58c326..b60211bc21e56f 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs
@@ -255,6 +255,118 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
}, NoAutoPingResponseHttp2Options);
}
+ [OuterLoop("Runs long")]
+ [Fact]
+ public async Task KeepAlivePing_ZeroConnectionLifetime_PingsStillWork()
+ {
+ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
+ handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
+ handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
+ handler.KeepAlivePingDelay = TimeSpan.FromSeconds(1);
+ handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime should still allow pings
+
+ using HttpClient client = new HttpClient(handler);
+ client.DefaultRequestVersion = HttpVersion.Version20;
+
+ // Warmup request to create connection:
+ HttpResponseMessage response0 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
+
+ // Actual request:
+ HttpResponseMessage response1 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
+
+ // Let connection live until server finishes:
+ await _serverFinished.Task.WaitAsync(TestTimeout);
+ },
+ async server =>
+ {
+ await EstablishConnectionAsync(server);
+
+ // Warmup the connection.
+ int streamId1 = await ReadRequestHeaderAsync();
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+ // Request under the test scope.
+ int streamId2 = await ReadRequestHeaderAsync();
+ Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+
+ // Simulate inactive period:
+ await Task.Delay(5_000);
+
+ // Even with zero lifetime, we should still receive pings for active streams:
+ // We may receive one RTT PING in response to HEADERS.
+ // Upon that, we expect to receive at least 1 keep alive PING:
+ Assert.True(_pingCounter > 1);
+
+ // Finish the response:
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+ await TerminateLoopbackConnectionAsync();
+
+ List unexpectedFrames = new List();
+ while (_framesChannel.Reader.Count > 0)
+ {
+ Frame unexpectedFrame = await _framesChannel.Reader.ReadAsync();
+ unexpectedFrames.Add(unexpectedFrame);
+ }
+
+ Assert.False(unexpectedFrames.Any(), "Received unexpected frames: \n" + string.Join('\n', unexpectedFrames.Select(f => f.ToString()).ToArray()));
+ }, NoAutoPingResponseHttp2Options);
+ }
+
+ [OuterLoop("Runs long")]
+ [Fact]
+ public async Task KeepAlivePing_ZeroConnectionLifetime_NoPingConfig_NoPingsSent()
+ {
+ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+ {
+ SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
+ // Don't configure KeepAlivePing settings
+ handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime
+
+ using HttpClient client = new HttpClient(handler);
+ client.DefaultRequestVersion = HttpVersion.Version20;
+
+ // Warmup request to create connection:
+ HttpResponseMessage response0 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
+
+ // Actual request:
+ HttpResponseMessage response1 = await client.GetAsync(uri);
+ Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
+
+ // Let connection live until server finishes:
+ await _serverFinished.Task.WaitAsync(TestTimeout);
+ },
+ async server =>
+ {
+ await EstablishConnectionAsync(server);
+
+ // Warmup the connection.
+ int streamId1 = await ReadRequestHeaderAsync();
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+ // Request under the test scope.
+ int streamId2 = await ReadRequestHeaderAsync();
+ Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+
+ // Simulate inactive period:
+ await Task.Delay(5_000);
+
+ // With zero lifetime and no ping config, we should not receive keep alive pings
+ // We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
+ Assert.True(_pingCounter <= 1);
+
+ // Finish the response:
+ await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+ await TerminateLoopbackConnectionAsync();
+ }, NoAutoPingResponseHttp2Options);
+ }
+
private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
{
try
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
deleted file mode 100644
index d698bcfd7b0048..00000000000000
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs
+++ /dev/null
@@ -1,247 +0,0 @@
-// 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.IO;
-using System.Linq;
-using System.Net.Test.Common;
-using System.Threading;
-using System.Threading.Channels;
-using System.Threading.Tasks;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace System.Net.Http.Functional.Tests
-{
- [Collection(nameof(DisableParallelization))]
- [ConditionalClass(typeof(SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test), nameof(IsSupported))]
- public sealed class SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test : HttpClientHandlerTestBase
- {
- public static readonly bool IsSupported = PlatformDetection.SupportsAlpn && PlatformDetection.IsNotBrowser;
-
- protected override Version UseVersion => HttpVersion20.Value;
-
- private int _pingCounter;
- private Http2LoopbackConnection _connection;
- private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1);
- private Channel _framesChannel = Channel.CreateUnbounded();
- private CancellationTokenSource _incomingFramesCts = new CancellationTokenSource();
- private Task _incomingFramesTask;
- private TaskCompletionSource _serverFinished = new TaskCompletionSource();
- private bool _sendPingResponse = true;
-
- private static Http2Options NoAutoPingResponseHttp2Options => new Http2Options() { EnableTransparentPingResponse = false };
-
- private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
-
- public SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test(ITestOutputHelper output) : base(output)
- {
- }
-
- [OuterLoop("Runs long")]
- [Fact]
- public async Task KeepAlivePing_ZeroConnectionLifetime_PingsStillWork()
- {
- await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
- {
- SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
- handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
- handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
- handler.KeepAlivePingDelay = TimeSpan.FromSeconds(1);
- handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime should still allow pings
-
- using HttpClient client = new HttpClient(handler);
- client.DefaultRequestVersion = HttpVersion.Version20;
-
- // Warmup request to create connection:
- HttpResponseMessage response0 = await client.GetAsync(uri);
- Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
-
- // Actual request:
- HttpResponseMessage response1 = await client.GetAsync(uri);
- Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
-
- // Let connection live until server finishes:
- await _serverFinished.Task.WaitAsync(TestTimeout);
- },
- async server =>
- {
- await EstablishConnectionAsync(server);
-
- // Warmup the connection.
- int streamId1 = await ReadRequestHeaderAsync();
- await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
-
- // Request under the test scope.
- int streamId2 = await ReadRequestHeaderAsync();
- Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
-
- // Simulate inactive period:
- await Task.Delay(5_000);
-
- // Even with zero lifetime, we should still receive pings for active streams:
- // We may receive one RTT PING in response to HEADERS.
- // Upon that, we expect to receive at least 1 keep alive PING:
- Assert.True(_pingCounter > 1);
-
- // Finish the response:
- await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
-
- await TerminateLoopbackConnectionAsync();
-
- List unexpectedFrames = new List();
- while (_framesChannel.Reader.Count > 0)
- {
- Frame unexpectedFrame = await _framesChannel.Reader.ReadAsync();
- unexpectedFrames.Add(unexpectedFrame);
- }
-
- Assert.False(unexpectedFrames.Any(), "Received unexpected frames: \n" + string.Join('\n', unexpectedFrames.Select(f => f.ToString()).ToArray()));
- }, NoAutoPingResponseHttp2Options);
- }
-
- [OuterLoop("Runs long")]
- [Fact]
- public async Task KeepAlivePing_ZeroConnectionLifetime_NoPingConfig_NoPingsSent()
- {
- await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
- {
- SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
- // Don't configure KeepAlivePing settings
- handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime
-
- using HttpClient client = new HttpClient(handler);
- client.DefaultRequestVersion = HttpVersion.Version20;
-
- // Warmup request to create connection:
- HttpResponseMessage response0 = await client.GetAsync(uri);
- Assert.Equal(HttpStatusCode.OK, response0.StatusCode);
-
- // Actual request:
- HttpResponseMessage response1 = await client.GetAsync(uri);
- Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
-
- // Let connection live until server finishes:
- await _serverFinished.Task.WaitAsync(TestTimeout);
- },
- async server =>
- {
- await EstablishConnectionAsync(server);
-
- // Warmup the connection.
- int streamId1 = await ReadRequestHeaderAsync();
- await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
-
- // Request under the test scope.
- int streamId2 = await ReadRequestHeaderAsync();
- Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
-
- // Simulate inactive period:
- await Task.Delay(5_000);
-
- // With zero lifetime and no ping config, we should not receive keep alive pings
- // We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
- Assert.True(_pingCounter <= 1);
-
- // Finish the response:
- await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
-
- await TerminateLoopbackConnectionAsync();
- }, NoAutoPingResponseHttp2Options);
- }
-
- private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
- {
- try
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- Frame frame = await _connection.ReadFrameAsync(cancellationToken);
-
- if (frame is null)
- {
- break;
- }
-
- if (frame is PingFrame pingFrame)
- {
- if (pingFrame.AckFlag)
- {
- _output?.WriteLine($"Received unexpected PING ACK ({pingFrame.Data})");
- await _framesChannel.Writer.WriteAsync(frame, cancellationToken);
- }
- else
- {
- _output?.WriteLine($"Received PING ({pingFrame.Data})");
- Interlocked.Increment(ref _pingCounter);
-
- if (_sendPingResponse)
- {
- await GuardConnectionWriteAsync(() => _connection.SendPingAckAsync(pingFrame.Data, cancellationToken), cancellationToken);
- }
- }
- }
- else if (frame is WindowUpdateFrame windowUpdateFrame)
- {
- _output?.WriteLine($"Received WINDOW_UPDATE");
- }
- else
- {
- //_output?.WriteLine($"Received {frame}");
- await _framesChannel.Writer.WriteAsync(frame, cancellationToken);
- }
- }
- }
- catch (OperationCanceledException)
- {
- }
-
- _output?.WriteLine("ProcessIncomingFramesAsync finished");
- await _connection.DisposeAsync();
- }
-
- private async Task EstablishConnectionAsync(Http2LoopbackServer server)
- {
- _connection = await server.EstablishConnectionAsync();
- _incomingFramesTask = ProcessIncomingFramesAsync(_incomingFramesCts.Token);
- }
-
- private async Task TerminateLoopbackConnectionAsync()
- {
- _serverFinished.SetResult();
- _incomingFramesCts.Cancel();
- await _incomingFramesTask;
- }
-
- private async Task GuardConnectionWriteAsync(Func action, CancellationToken cancellationToken = default)
- {
- await _writeSemaphore.WaitAsync(cancellationToken);
- await action();
- _writeSemaphore.Release();
- }
-
- private async Task ReadRequestHeaderFrameAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default)
- {
- // Receive HEADERS frame for request.
- Frame frame = await _framesChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
- if (frame == null)
- {
- throw new IOException("Failed to read Headers frame.");
- }
-
- Assert.Equal(FrameType.Headers, frame.Type);
- Assert.Equal(FrameFlags.EndHeaders, frame.Flags & FrameFlags.EndHeaders);
- if (expectEndOfStream)
- {
- Assert.Equal(FrameFlags.EndStream, frame.Flags & FrameFlags.EndStream);
- }
- return (HeadersFrame)frame;
- }
-
- private async Task ReadRequestHeaderAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default)
- {
- HeadersFrame frame = await ReadRequestHeaderFrameAsync(expectEndOfStream, cancellationToken);
- return frame.StreamId;
- }
- }
-}
\ No newline at end of file
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
index 74811f21bc9926..340819fc688edb 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
@@ -179,7 +179,6 @@
-