HTTP/2: Add automatic downgrade to HTTP/1.1 for Windows authentication#123827
HTTP/2: Add automatic downgrade to HTTP/1.1 for Windows authentication#123827
Conversation
src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Outdated
Show resolved
Hide resolved
.../System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
Show resolved
Hide resolved
|
The PR change as-is would only handle requests without a body, and automatically retry on 1.1.
Alternatively, we could disable HTTP/2 on the connection pool if we reach this case, so future requests would just start with 1.1. This also has problems:
I'm leaning towards trying out the alternative approach. |
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
- 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>
- 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>
- 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>
When an HTTP/2 request receives a session-based auth challenge (NTLM/Negotiate), disable HTTP/2 on the connection pool so all future requests use HTTP/1.1 directly. This avoids extra roundtrips for subsequent requests and ensures POST requests work after the initial downgrade. - Add DisableHttp2() method to HttpConnectionPool - Catch RetryOnSessionAuthenticationChallenge in SendWithVersionDetectionAndRetryAsync and call DisableHttp2() before retrying - Rewrite tests using HttpAgnosticLoopbackServer for proper HTTP/2→1.1 testing - Add tests for: pool downgrade, subsequent requests, exact version policy, content requests, and non-auth HTTP/2 responses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of disabling HTTP/2 entirely (which would break HTTP/2-only requests), add a _http2SessionAuthSeen flag that only affects requests with RequestVersionOrLower policy. HTTP/2-only requests (RequestVersionExact, RequestVersionOrHigher) continue to work on HTTP/2 as before. Also update GetSslOptionsForRequest to avoid negotiating h2 ALPN for downgradeable requests after session auth has been seen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Make _http2SessionAuthSeen volatile for cross-thread visibility - Always set the pool flag when a session auth challenge is detected, even for requests that can't be retried (e.g. POST with content). This ensures subsequent downgradeable requests benefit immediately. - Move flag-setting from catch handler to Http2Connection.SendAsync - Add test: POST triggers flag, subsequent GET uses HTTP/1.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of inspecting the request to decide ALPN, pass the intended protocol version into ConnectAsync. This avoids the bug where a queued HTTP/2 connection establishment could pick up a downgradeable request and negotiate HTTP/1.1 ALPN, which would trigger HandleHttp11Downgrade and disable HTTP/2 for the entire pool. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f355c6e to
bcd9d4b
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates SocketsHttpHandler to automatically fall back from HTTP/2 to HTTP/1.1 when an HTTP/2 response indicates a session-based Windows authentication challenge (NTLM/Negotiate), enabling successful authentication for retryable requests while preserving behavior when downgrade is not allowed.
Changes:
- Added a new retry signal (
RequestRetryType.RetryOnSessionAuthenticationChallenge) to represent auth-driven protocol fallback. - Detected NTLM/Negotiate challenges in
Http2Connection.SendAsync()and triggered an HTTP/1.1 retry (when safe), while also marking the pool to prefer HTTP/1.1 for future downgradeable requests. - Updated connection establishment to explicitly control ALPN selection based on whether the connection is intended for HTTP/2, and added functional tests for downgrade behavior and constraints.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs | Adds functional tests verifying HTTP/2-to-HTTP/1.1 downgrade behavior and non-downgrade constraints. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs | Detects session-auth challenges on HTTP/2 and signals retry/pool behavior updates. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs | Skips HTTP/2 after session-auth is seen (for downgradeable requests) and handles the new retry type by retrying on HTTP/1.1. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http2.cs | Adds pool flag tracking for session-auth challenges. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http1.cs | Updates connection creation to pass the new “isForHttp2” signal into shared connect logic. |
| src/libraries/System.Net.Http/src/System/Net/Http/RequestRetryType.cs | Adds a new retry enum value for session-auth downgrade retries. |
You can also share your feedback on Copilot code review. Take the survey.
Description
Session-based authentication (NTLM/Negotiate) requires persistent connections and fails over HTTP/2. When SocketsHttpHandler receives a 401 with these schemes on HTTP/2, it now automatically retries on HTTP/1.1 if the request is retryable.
Changes
Core retry mechanism:
RequestRetryType.RetryOnSessionAuthenticationChallengeto signal auth-driven downgradesHttp2Connection.SendAsync()detects session auth challenges (401 + NTLM/Negotiate) and checks version policy before attempting retryHttpConnectionPool.SendWithVersionDetectionAndRetryAsync()catches and retries on HTTP/1.1Safety constraints:
request.Content == null)request.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower)Test coverage:
RequestVersionExact)Example
Limitations
This implementation only handles requests without content (e.g., GET, HEAD, DELETE without body) when the version policy allows downgrade. POST and PUT requests with content, or requests with
RequestVersionExactpolicy, will receive a 401 response and must be handled by the application, as we cannot safely retry requests with content that may not be rewindable or when downgrade is not permitted.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.