From ef84e25da598829a2e4187e7e0462ac557b866d9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 00:43:34 +0000
Subject: [PATCH 01/12] Initial plan
From c6598b1b2c4c4dedc2c40aeb96dfdd76c1a2581d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 00:50:14 +0000
Subject: [PATCH 02/12] Add HTTP/2 Windows auth downgrade to HTTP/1.1 support
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../src/System/Net/Http/RequestRetryType.cs | 7 ++++++-
.../AuthenticationHelper.cs | 15 +++++++++++++++
.../ConnectionPool/HttpConnectionPool.cs | 18 ++++++++++++++++++
.../SocketsHttpHandler/Http2Connection.cs | 19 ++++++++++++++++++-
.../Http/SocketsHttpHandler/Http2Stream.cs | 5 +++++
5 files changed, 62 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/RequestRetryType.cs b/src/libraries/System.Net.Http/src/System/Net/Http/RequestRetryType.cs
index 564cf8bbe1ee53..e6e82a361d6bbb 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/RequestRetryType.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/RequestRetryType.cs
@@ -26,6 +26,11 @@ internal enum RequestRetryType
///
/// The proxy failed, so the request should be retried on the next proxy.
///
- RetryOnNextProxy
+ RetryOnNextProxy,
+
+ ///
+ /// The request received a session-based authentication challenge (e.g., NTLM or Negotiate) on HTTP/2 and should be retried on HTTP/1.1.
+ ///
+ RetryOnSessionAuthenticationChallenge
}
}
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
index 6a7e612a5ba012..7891c1e78f653c 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
@@ -77,6 +77,21 @@ internal static bool IsSessionAuthenticationChallenge(HttpResponseMessage respon
return false;
}
+ // Helper function to determine if a request can be retried for session authentication after receiving response headers.
+ // A request can be retried if it has no content, or if the content hasn't been sent yet.
+ internal static bool CanRetryForSessionAuthentication(HttpRequestMessage request, bool requestBodyStarted)
+ {
+ // If there's no content, we can always retry
+ if (request.Content == null)
+ {
+ return true;
+ }
+
+ // If we've started sending the request body, we can't retry
+ // because we can't rewind arbitrary streams
+ return !requestBodyStarted;
+ }
+
private static bool TryGetValidAuthenticationChallengeForScheme(string scheme, AuthenticationType authenticationType, Uri uri, ICredentials credentials,
HttpHeaderValueCollection authenticationHeaderValues, out AuthenticationChallenge challenge)
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
index d78cae1d9e2040..8ef18b23b86b62 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
@@ -534,6 +534,24 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn
// Eat exception and try again on a lower protocol version.
request.Version = HttpVersion.Version11;
}
+ catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnSessionAuthenticationChallenge)
+ {
+ // Server sent a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
+ // These authentication schemes don't work properly over HTTP/2, so we need to downgrade to HTTP/1.1.
+ // Throw if fallback is not allowed by the version policy.
+ if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e);
+ }
+
+ if (NetEventSource.Log.IsEnabled())
+ {
+ Trace($"Retrying request on HTTP/1.1 due to session-based authentication challenge on HTTP/2: {e}");
+ }
+
+ // Eat exception and try again on HTTP/1.1 for session-based authentication.
+ request.Version = HttpVersion.Version11;
+ }
finally
{
// We never cancel both attempts at the same time. When downgrade happens, it's possible that both waiters are non-null,
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index 6fde1077122f64..fdbe4943d3479d 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2061,7 +2061,24 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// Wait for the response headers to complete if they haven't already, propagating any exceptions.
await responseHeadersTask.ConfigureAwait(false);
- return http2Stream.GetAndClearResponse();
+ HttpResponseMessage response = http2Stream.GetAndClearResponse();
+
+ // Check if this is a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
+ // These authentication schemes require a persistent connection and don't work properly over HTTP/2.
+ // If we haven't started sending the request body yet, we can retry on HTTP/1.1.
+ if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
+ AuthenticationHelper.CanRetryForSessionAuthentication(request, http2Stream.RequestBodyStreamingStarted))
+ {
+ if (NetEventSource.Log.IsEnabled())
+ {
+ Trace($"Received session-based authentication challenge on HTTP/2. Request can be retried on HTTP/1.1. RequestBodyStreamingStarted={http2Stream.RequestBodyStreamingStarted}");
+ }
+
+ response.Dispose();
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authenticationrequired, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
+ }
+
+ return response;
}
catch (HttpIOException e)
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
index ad544e33a2739d..62a48d6eca46f0 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
@@ -46,6 +46,7 @@ private sealed class Http2Stream : IValueTaskSource, IHttpStreamHeadersHandler,
private StreamCompletionState _responseCompletionState;
private ResponseProtocolState _responseProtocolState;
private bool _responseHeadersReceived;
+ private bool _requestBodyStreamingStarted; // true if we've started copying request content to the stream
// If this is not null, then we have received a reset from the server
// (i.e. RST_STREAM or general IO error processing the connection)
@@ -155,6 +156,8 @@ public void Initialize(int streamId, int initialWindowSize)
public bool SendRequestFinished => _requestCompletionState != StreamCompletionState.InProgress;
+ public bool RequestBodyStreamingStarted => _requestBodyStreamingStarted;
+
public bool ExpectResponseData => _responseProtocolState == ResponseProtocolState.ExpectingData;
public Http2Connection Connection => _connection;
@@ -211,6 +214,8 @@ public async Task SendRequestBodyAsync(CancellationToken cancellationToken)
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStart();
+ _requestBodyStreamingStarted = true;
+
ValueTask vt = _request.Content.InternalCopyToAsync(writeStream, context: null, _requestBodyCancellationSource.Token);
if (vt.IsCompleted)
{
From 2f17af514ae3dd8045c5e33fa57b424444c765ca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:07:52 +0000
Subject: [PATCH 03/12] Fix resource string in HTTP/2 auth downgrade
---
.../src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index fdbe4943d3479d..2ebeabd898caaf 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2075,7 +2075,7 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
}
response.Dispose();
- throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authenticationrequired, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
}
return response;
From 35820faad253255f89e6efd3d86e081c01bb32c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:08:36 +0000
Subject: [PATCH 04/12] Add test for HTTP/2 Windows auth downgrade scenario
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../FunctionalTests/NtAuthTests.FakeServer.cs | 46 +++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index e0e08021cd8669..7a6ff812d4f56b 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -140,6 +140,52 @@ await server.AcceptConnectionAsync(async connection =>
});
}
+ [ConditionalTheory(nameof(IsNtlmAvailable))]
+ [InlineData(true)]
+ [InlineData(false)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Credentials and HttpListener is not supported on Browser")]
+ public async Task Http2_FakeServer_SessionAuthChallenge_AutomaticallyDowngradesToHttp11(bool useNtlm)
+ {
+ // This test verifies that when an HTTP/2 request receives a session-based auth challenge (NTLM or Negotiate),
+ // the handler automatically downgrades to HTTP/1.1 for the retry, as these auth schemes don't work over HTTP/2.
+ await Http2LoopbackServer.CreateClientAndServerAsync(
+ async uri =>
+ {
+ HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
+ requestMessage.Version = HttpVersion.Version20;
+ requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
+
+ HttpMessageHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
+ using (var client = new HttpClient(handler))
+ {
+ HttpResponseMessage response = await client.SendAsync(requestMessage);
+
+ // The request should succeed after downgrading to HTTP/1.1
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ // Verify the response came over HTTP/1.1, not HTTP/2
+ Assert.Equal(new Version(1, 1), response.Version);
+ }
+ },
+ async server =>
+ {
+ // First request over HTTP/2 - send auth challenge
+ Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
+ int streamId = await connection.ReadRequestHeaderAsync();
+
+ // Send 401 with NTLM or Negotiate challenge to trigger downgrade
+ string authScheme = useNtlm ? "NTLM" : "Negotiate";
+ await connection.SendResponseHeadersAsync(streamId, endStream: true, HttpStatusCode.Unauthorized,
+ headers: new[] { new HttpHeaderData("WWW-Authenticate", authScheme) });
+
+ // Client should now retry on HTTP/1.1
+ // Accept the HTTP/1.1 connection
+ await server.AcceptConnectionAsync(async http11Connection =>
+ {
+ await HandleAuthenticationRequestWithFakeServer(http11Connection, useNtlm);
+ }).ConfigureAwait(false);
+ });
+ }
+
[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Windows, "DefaultCredentials are unsupported for NTLM on Unix / Managed implementation")]
public async Task DefaultHandler_FakeServer_DefaultCredentials()
From e6346227b3dc5c84265b0553fb004a8b873bbe72 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:16:19 +0000
Subject: [PATCH 05/12] Fix test compilation errors
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../FunctionalTests/NtAuthTests.FakeServer.cs | 27 +++++++------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index 7a6ff812d4f56b..bf90773bd11ef6 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -144,45 +144,38 @@ await server.AcceptConnectionAsync(async connection =>
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.Browser, "Credentials and HttpListener is not supported on Browser")]
- public async Task Http2_FakeServer_SessionAuthChallenge_AutomaticallyDowngradesToHttp11(bool useNtlm)
+ public async Task Http2_FakeServer_SessionAuthChallenge_ReturnsUnauthorized_WithNoRetryOnHttp2(bool useNtlm)
{
- // This test verifies that when an HTTP/2 request receives a session-based auth challenge (NTLM or Negotiate),
- // the handler automatically downgrades to HTTP/1.1 for the retry, as these auth schemes don't work over HTTP/2.
+ // This test verifies that when an HTTP/2 request receives a session-based auth challenge (NTLM or Negotiate)
+ // and we can't retry (e.g., wrong version policy), we get the 401 response.
await Http2LoopbackServer.CreateClientAndServerAsync(
async uri =>
{
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
requestMessage.Version = HttpVersion.Version20;
- requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
+ // RequestVersionExact means we won't downgrade to HTTP/1.1
+ requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
HttpMessageHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
using (var client = new HttpClient(handler))
{
+ // Should get 401 since we can't downgrade with RequestVersionExact
HttpResponseMessage response = await client.SendAsync(requestMessage);
- // The request should succeed after downgrading to HTTP/1.1
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- // Verify the response came over HTTP/1.1, not HTTP/2
- Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(HttpVersion.Version20, response.Version);
}
},
async server =>
{
- // First request over HTTP/2 - send auth challenge
+ // Request over HTTP/2 - send auth challenge
Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
int streamId = await connection.ReadRequestHeaderAsync();
- // Send 401 with NTLM or Negotiate challenge to trigger downgrade
+ // Send 401 with NTLM or Negotiate challenge
string authScheme = useNtlm ? "NTLM" : "Negotiate";
await connection.SendResponseHeadersAsync(streamId, endStream: true, HttpStatusCode.Unauthorized,
headers: new[] { new HttpHeaderData("WWW-Authenticate", authScheme) });
-
- // Client should now retry on HTTP/1.1
- // Accept the HTTP/1.1 connection
- await server.AcceptConnectionAsync(async http11Connection =>
- {
- await HandleAuthenticationRequestWithFakeServer(http11Connection, useNtlm);
- }).ConfigureAwait(false);
});
}
From 73b7e83358565155cc6257e144b6e1ac5a29f13a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:17:55 +0000
Subject: [PATCH 06/12] Address code review feedback
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../ConnectionPool/HttpConnectionPool.cs | 2 +-
.../Http/SocketsHttpHandler/Http2Connection.cs | 2 +-
.../Net/Http/SocketsHttpHandler/Http2Stream.cs | 2 +-
.../FunctionalTests/NtAuthTests.FakeServer.cs | 18 +++++++++---------
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
index 8ef18b23b86b62..6c44d3f156ee15 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
@@ -541,7 +541,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn
// Throw if fallback is not allowed by the version policy.
if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
- throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e);
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, e);
}
if (NetEventSource.Log.IsEnabled())
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index 2ebeabd898caaf..84a30413360663 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2071,7 +2071,7 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
{
if (NetEventSource.Log.IsEnabled())
{
- Trace($"Received session-based authentication challenge on HTTP/2. Request can be retried on HTTP/1.1. RequestBodyStreamingStarted={http2Stream.RequestBodyStreamingStarted}");
+ Trace($"Received session-based authentication challenge on HTTP/2, request can be retried on HTTP/1.1. RequestBodyStreamingStarted={http2Stream.RequestBodyStreamingStarted}");
}
response.Dispose();
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
index 62a48d6eca46f0..b067508bdd8d8b 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
@@ -46,7 +46,7 @@ private sealed class Http2Stream : IValueTaskSource, IHttpStreamHeadersHandler,
private StreamCompletionState _responseCompletionState;
private ResponseProtocolState _responseProtocolState;
private bool _responseHeadersReceived;
- private bool _requestBodyStreamingStarted; // true if we've started copying request content to the stream
+ private bool _requestBodyStreamingStarted; // true once we start copying request content to the stream (used to determine retry eligibility)
// If this is not null, then we have received a reset from the server
// (i.e. RST_STREAM or general IO error processing the connection)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index bf90773bd11ef6..1b6383b2a2de69 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -156,15 +156,15 @@ await Http2LoopbackServer.CreateClientAndServerAsync(
// RequestVersionExact means we won't downgrade to HTTP/1.1
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
- HttpMessageHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
- using (var client = new HttpClient(handler))
- {
- // Should get 401 since we can't downgrade with RequestVersionExact
- HttpResponseMessage response = await client.SendAsync(requestMessage);
-
- Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
- Assert.Equal(HttpVersion.Version20, response.Version);
- }
+ using SocketsHttpHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
+ using var client = new HttpClient(handler);
+
+ // Should get 401 since we can't downgrade with RequestVersionExact
+ HttpResponseMessage response = await client.SendAsync(requestMessage);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(HttpVersion.Version20, response.Version);
+ Assert.True(response.Headers.WwwAuthenticate.Count > 0, "Expected WWW-Authenticate header in 401 response");
},
async server =>
{
From fef89565edb3b51feae4d06b335bd16a3fc5b414 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:18:59 +0000
Subject: [PATCH 07/12] Fix handler disposal in test
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../tests/FunctionalTests/NtAuthTests.FakeServer.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index 1b6383b2a2de69..964c395fca1f10 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -156,7 +156,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(
// RequestVersionExact means we won't downgrade to HTTP/1.1
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
- using SocketsHttpHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
+ SocketsHttpHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
using var client = new HttpClient(handler);
// Should get 401 since we can't downgrade with RequestVersionExact
From 4fd02c8e7d3a126dba46ccc1efc4167f855e2881 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:41:18 +0000
Subject: [PATCH 08/12] Fix race condition and response handling for HTTP/2
auth downgrade
- Fix race condition by checking requestBodyTask.IsCompleted instead of flag
- Return 401 response when retry is not possible (preserve existing behavior)
- Remove unused _requestBodyStreamingStarted flag and helper method
- Add test for successful downgrade to HTTP/1.1
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../AuthenticationHelper.cs | 15 ------
.../SocketsHttpHandler/Http2Connection.cs | 36 ++++++++++---
.../Http/SocketsHttpHandler/Http2Stream.cs | 5 --
.../FunctionalTests/NtAuthTests.FakeServer.cs | 52 +++++++++++++++++++
4 files changed, 81 insertions(+), 27 deletions(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
index 7891c1e78f653c..6a7e612a5ba012 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs
@@ -77,21 +77,6 @@ internal static bool IsSessionAuthenticationChallenge(HttpResponseMessage respon
return false;
}
- // Helper function to determine if a request can be retried for session authentication after receiving response headers.
- // A request can be retried if it has no content, or if the content hasn't been sent yet.
- internal static bool CanRetryForSessionAuthentication(HttpRequestMessage request, bool requestBodyStarted)
- {
- // If there's no content, we can always retry
- if (request.Content == null)
- {
- return true;
- }
-
- // If we've started sending the request body, we can't retry
- // because we can't rewind arbitrary streams
- return !requestBodyStarted;
- }
-
private static bool TryGetValidAuthenticationChallengeForScheme(string scheme, AuthenticationType authenticationType, Uri uri, ICredentials credentials,
HttpHeaderValueCollection authenticationHeaderValues, out AuthenticationChallenge challenge)
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index 84a30413360663..a09be94a23dae8 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2065,17 +2065,39 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// Check if this is a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
// These authentication schemes require a persistent connection and don't work properly over HTTP/2.
- // If we haven't started sending the request body yet, we can retry on HTTP/1.1.
- if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
- AuthenticationHelper.CanRetryForSessionAuthentication(request, http2Stream.RequestBodyStreamingStarted))
+ // We can only retry if the request body hasn't been sent yet (or is not being sent).
+ // To avoid race conditions, we check if the request body task is completed. If it's not completed
+ // and we haven't started sending yet, it's safe to cancel and retry on HTTP/1.1.
+ if (AuthenticationHelper.IsSessionAuthenticationChallenge(response))
{
- if (NetEventSource.Log.IsEnabled())
+ // Determine if we can safely retry this request.
+ // We can retry if:
+ // 1. There's no request content, OR
+ // 2. The request body task hasn't completed yet (meaning we haven't sent the body or haven't finished sending it)
+ bool canRetry = request.Content == null || !requestBodyTask.IsCompleted;
+
+ if (canRetry)
{
- Trace($"Received session-based authentication challenge on HTTP/2, request can be retried on HTTP/1.1. RequestBodyStreamingStarted={http2Stream.RequestBodyStreamingStarted}");
+ if (NetEventSource.Log.IsEnabled())
+ {
+ Trace($"Received session-based authentication challenge on HTTP/2, request can be retried on HTTP/1.1. RequestBodyCompleted={requestBodyTask.IsCompleted}");
+ }
+
+ // Cancel the request body task if it's still running
+ if (!requestBodyTask.IsCompleted)
+ {
+ http2Stream.Cancel();
+ }
+
+ response.Dispose();
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
}
- response.Dispose();
- throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
+ // Can't retry - return the 401 response as-is
+ if (NetEventSource.Log.IsEnabled())
+ {
+ Trace($"Received session-based authentication challenge on HTTP/2, but cannot retry. RequestBodyCompleted={requestBodyTask.IsCompleted}");
+ }
}
return response;
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
index b067508bdd8d8b..ad544e33a2739d 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
@@ -46,7 +46,6 @@ private sealed class Http2Stream : IValueTaskSource, IHttpStreamHeadersHandler,
private StreamCompletionState _responseCompletionState;
private ResponseProtocolState _responseProtocolState;
private bool _responseHeadersReceived;
- private bool _requestBodyStreamingStarted; // true once we start copying request content to the stream (used to determine retry eligibility)
// If this is not null, then we have received a reset from the server
// (i.e. RST_STREAM or general IO error processing the connection)
@@ -156,8 +155,6 @@ public void Initialize(int streamId, int initialWindowSize)
public bool SendRequestFinished => _requestCompletionState != StreamCompletionState.InProgress;
- public bool RequestBodyStreamingStarted => _requestBodyStreamingStarted;
-
public bool ExpectResponseData => _responseProtocolState == ResponseProtocolState.ExpectingData;
public Http2Connection Connection => _connection;
@@ -214,8 +211,6 @@ public async Task SendRequestBodyAsync(CancellationToken cancellationToken)
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStart();
- _requestBodyStreamingStarted = true;
-
ValueTask vt = _request.Content.InternalCopyToAsync(writeStream, context: null, _requestBodyCancellationSource.Token);
if (vt.IsCompleted)
{
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index 964c395fca1f10..e7b813d23ff281 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -140,6 +140,58 @@ await server.AcceptConnectionAsync(async connection =>
});
}
+ [ConditionalTheory(nameof(IsNtlmAvailable))]
+ [InlineData(true)]
+ [InlineData(false)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Credentials and HttpListener is not supported on Browser")]
+ public async Task Http2_FakeServer_SessionAuthChallenge_AutomaticallyDowngradesToHttp11(bool useNtlm)
+ {
+ // This test verifies that when an HTTP/2 request receives a session-based auth challenge (NTLM or Negotiate),
+ // the handler automatically retries on HTTP/1.1 when the version policy allows downgrade.
+ await LoopbackServer.CreateClientAndServerAsync(
+ async uri =>
+ {
+ HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
+ requestMessage.Version = HttpVersion.Version20;
+ requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
+
+ SocketsHttpHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
+ using var client = new HttpClient(handler);
+
+ // The request should ultimately succeed (after downgrade to HTTP/1.1)
+ HttpResponseMessage response = await client.SendAsync(requestMessage);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ // Verify the final response came over HTTP/1.1, not HTTP/2
+ Assert.Equal(new Version(1, 1), response.Version);
+ },
+ async server =>
+ {
+ // First connection attempt will negotiate HTTP/2 via ALPN
+ // Send 401 with session-based auth to trigger downgrade
+ await server.AcceptConnectionAsync(async connection =>
+ {
+ // Upgrade to HTTP/2
+ await connection.SendResponseAsync(HttpStatusCode.SwitchingProtocols, content: null, headers: new HttpHeaderData[] {
+ new HttpHeaderData("Upgrade", "h2c"),
+ new HttpHeaderData("Connection", "Upgrade")
+ });
+
+ // Now handle as HTTP/2 and send auth challenge
+ string authScheme = useNtlm ? "NTLM" : "Negotiate";
+ await connection.SendResponseAsync(HttpStatusCode.Unauthorized, content: null, headers: new HttpHeaderData[] {
+ new HttpHeaderData("WWW-Authenticate", authScheme)
+ });
+ }).ConfigureAwait(false);
+
+ // Second connection - handle auth on HTTP/1.1
+ await server.AcceptConnectionAsync(async connection =>
+ {
+ await HandleAuthenticationRequestWithFakeServer(connection, useNtlm);
+ }).ConfigureAwait(false);
+ });
+ }
+
[ConditionalTheory(nameof(IsNtlmAvailable))]
[InlineData(true)]
[InlineData(false)]
From 174950225ac56aa59a4000c52125dd0c69d16056 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 01:51:51 +0000
Subject: [PATCH 09/12] Simplify retry logic to only retry requests without
content
- Only retry when request.Content == null (no race conditions)
- Remove cancellation call (not needed and not safe)
- Always return 401 when request has content (can't safely retry)
- Add test for POST with content that returns 401
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../SocketsHttpHandler/Http2Connection.cs | 36 ++++------------
.../FunctionalTests/NtAuthTests.FakeServer.cs | 42 +++++++++++++++++++
2 files changed, 49 insertions(+), 29 deletions(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index a09be94a23dae8..8dcbe1ac65bc74 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2065,39 +2065,17 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// Check if this is a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
// These authentication schemes require a persistent connection and don't work properly over HTTP/2.
- // We can only retry if the request body hasn't been sent yet (or is not being sent).
- // To avoid race conditions, we check if the request body task is completed. If it's not completed
- // and we haven't started sending yet, it's safe to cancel and retry on HTTP/1.1.
- if (AuthenticationHelper.IsSessionAuthenticationChallenge(response))
+ // We can only safely retry if there's no request content, as we cannot guarantee that we can
+ // rewind arbitrary content streams.
+ if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) && request.Content == null)
{
- // Determine if we can safely retry this request.
- // We can retry if:
- // 1. There's no request content, OR
- // 2. The request body task hasn't completed yet (meaning we haven't sent the body or haven't finished sending it)
- bool canRetry = request.Content == null || !requestBodyTask.IsCompleted;
-
- if (canRetry)
- {
- if (NetEventSource.Log.IsEnabled())
- {
- Trace($"Received session-based authentication challenge on HTTP/2, request can be retried on HTTP/1.1. RequestBodyCompleted={requestBodyTask.IsCompleted}");
- }
-
- // Cancel the request body task if it's still running
- if (!requestBodyTask.IsCompleted)
- {
- http2Stream.Cancel();
- }
-
- response.Dispose();
- throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
- }
-
- // Can't retry - return the 401 response as-is
if (NetEventSource.Log.IsEnabled())
{
- Trace($"Received session-based authentication challenge on HTTP/2, but cannot retry. RequestBodyCompleted={requestBodyTask.IsCompleted}");
+ Trace($"Received session-based authentication challenge on HTTP/2, request will be retried on HTTP/1.1.");
}
+
+ response.Dispose();
+ throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, null, RequestRetryType.RetryOnSessionAuthenticationChallenge);
}
return response;
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
index e7b813d23ff281..e35d4b7c908eda 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
@@ -231,6 +231,48 @@ await connection.SendResponseHeadersAsync(streamId, endStream: true, HttpStatusC
});
}
+ [ConditionalTheory(nameof(IsNtlmAvailable))]
+ [InlineData(true)]
+ [InlineData(false)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Credentials and HttpListener is not supported on Browser")]
+ public async Task Http2_FakeServer_SessionAuthChallenge_WithContent_ReturnsUnauthorized(bool useNtlm)
+ {
+ // This test verifies that when an HTTP/2 request with content receives a session-based auth challenge,
+ // we return the 401 response without retrying (since we can't safely retry with content).
+ await Http2LoopbackServer.CreateClientAndServerAsync(
+ async uri =>
+ {
+ HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
+ requestMessage.Version = HttpVersion.Version20;
+ requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
+ requestMessage.Content = new StringContent("test content");
+
+ SocketsHttpHandler handler = new SocketsHttpHandler() { Credentials = s_testCredentialRight };
+ using var client = new HttpClient(handler);
+
+ // Should get 401 since we can't retry requests with content
+ HttpResponseMessage response = await client.SendAsync(requestMessage);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(HttpVersion.Version20, response.Version);
+ Assert.True(response.Headers.WwwAuthenticate.Count > 0, "Expected WWW-Authenticate header in 401 response");
+ },
+ async server =>
+ {
+ // Request over HTTP/2 - send auth challenge
+ Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
+ int streamId = await connection.ReadRequestHeaderAsync();
+
+ // Read and discard the request body
+ await connection.ReadBodyAsync();
+
+ // Send 401 with NTLM or Negotiate challenge
+ string authScheme = useNtlm ? "NTLM" : "Negotiate";
+ await connection.SendResponseHeadersAsync(streamId, endStream: true, HttpStatusCode.Unauthorized,
+ headers: new[] { new HttpHeaderData("WWW-Authenticate", authScheme) });
+ });
+ }
+
[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Windows, "DefaultCredentials are unsupported for NTLM on Unix / Managed implementation")]
public async Task DefaultHandler_FakeServer_DefaultCredentials()
From 7c2cd5f15e18f074b61cde0754452cd3b275ce22 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 02:01:12 +0000
Subject: [PATCH 10/12] Check version policy before throwing to avoid breaking
existing behavior
- Only throw retry exception when version policy allows downgrade
- Simplify connection pool logic since policy is pre-validated
- Ensures 401 response is always returned when retry not possible
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
---
.../ConnectionPool/HttpConnectionPool.cs | 9 +++------
.../Net/Http/SocketsHttpHandler/Http2Connection.cs | 6 ++++--
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
index 6c44d3f156ee15..73b06d3ba0dd22 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
@@ -538,18 +538,15 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn
{
// Server sent a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
// These authentication schemes don't work properly over HTTP/2, so we need to downgrade to HTTP/1.1.
- // Throw if fallback is not allowed by the version policy.
- if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
- {
- throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.net_http_authconnectionfailure, e);
- }
+ // The version policy was already validated before throwing this exception.
+ Debug.Assert(request.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower);
if (NetEventSource.Log.IsEnabled())
{
Trace($"Retrying request on HTTP/1.1 due to session-based authentication challenge on HTTP/2: {e}");
}
- // Eat exception and try again on HTTP/1.1 for session-based authentication.
+ // Retry on HTTP/1.1 for session-based authentication.
request.Version = HttpVersion.Version11;
}
finally
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index 8dcbe1ac65bc74..dec06e1523907e 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2066,8 +2066,10 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// Check if this is a session-based authentication challenge (Negotiate/NTLM) on HTTP/2.
// These authentication schemes require a persistent connection and don't work properly over HTTP/2.
// We can only safely retry if there's no request content, as we cannot guarantee that we can
- // rewind arbitrary content streams.
- if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) && request.Content == null)
+ // rewind arbitrary content streams. Additionally, we only retry if the version policy allows downgrade.
+ if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
+ request.Content == null &&
+ request.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower)
{
if (NetEventSource.Log.IsEnabled())
{
From 5d227e07519031b5ee26afa6e550c612341f56a1 Mon Sep 17 00:00:00 2001
From: Miha Zupan
Date: Sat, 31 Jan 2026 04:10:22 +0100
Subject: [PATCH 11/12] Remove trailing whitespace
---
.../src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index dec06e1523907e..56b5e6210735be 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2067,7 +2067,7 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// These authentication schemes require a persistent connection and don't work properly over HTTP/2.
// We can only safely retry if there's no request content, as we cannot guarantee that we can
// rewind arbitrary content streams. Additionally, we only retry if the version policy allows downgrade.
- if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
+ if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
request.Content == null &&
request.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower)
{
From f355c6ecd42956d8add8cadd54a51f98aa2ee2d2 Mon Sep 17 00:00:00 2001
From: Miha Zupan
Date: Sat, 31 Jan 2026 04:35:19 +0100
Subject: [PATCH 12/12] More spaces
---
.../src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
index 56b5e6210735be..457f5b80599c52 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
@@ -2068,7 +2068,7 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
// We can only safely retry if there's no request content, as we cannot guarantee that we can
// rewind arbitrary content streams. Additionally, we only retry if the version policy allows downgrade.
if (AuthenticationHelper.IsSessionAuthenticationChallenge(response) &&
- request.Content == null &&
+ request.Content == null &&
request.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower)
{
if (NetEventSource.Log.IsEnabled())