Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[HttpClient] ntlm regression on authPersistNonNTLM=false connections with reset()#64349

Merged
nicolas-grekas merged 1 commit into
symfony:6.4symfony/symfony:6.4from
Dooij:ntlm-forbid-reuseDooij/symfony:ntlm-forbid-reuseCopy head branch name to clipboard
May 23, 2026
Merged

[HttpClient] ntlm regression on authPersistNonNTLM=false connections with reset()#64349
nicolas-grekas merged 1 commit into
symfony:6.4symfony/symfony:6.4from
Dooij:ntlm-forbid-reuseDooij/symfony:ntlm-forbid-reuseCopy head branch name to clipboard

Conversation

@Dooij
Copy link
Copy Markdown
Contributor

@Dooij Dooij commented May 23, 2026

Q A
Branch? 6.4
Bug fix? yes
New feature? no
Deprecations? no
Issues -
License MIT

This fixes a regression introduced by #64046.

#64046 moved the connection cache off the curl share handle. That makes max_host_connections actually work, which was the goal, but it also breaks auth_ntlm against servers that don't persist NTLM authentication across requests on the same TCP connection. I ran into the issue calling a service configured as authPersistNonNTLM=false.

The first request succeeds: libcurl does the Type 1/2/3 handshake and gets a 201.
The second request reuses that TCP connection, libcurl's NTLM state machine considers it already authenticated and skips the handshake, IIS treats each request independently and returns 401 with a fresh WWW-Authenticate: NTLM challenge, which isnt picked up as a new handshare by libcurl anymore.

My example:

  Request 1 (~1.5s, success):
    > Authorization: NTLM <Type 1>
    < HTTP/1.1 401 + WWW-Authenticate: NTLM <Type 2>
    > Authorization: NTLM <Type 3>
    < HTTP/1.1 201 Created
    * Connection #0 left intact

  Request 2 (~18ms, 401):
    > (reuses connection #0, no handshake)
    < HTTP/1.1 401 + WWW-Authenticate: NTLM <Type 2>
    (no retry)

The underlying NTLM-on-reused-connection problem predates #64046: any consumer making two or more NTLM requests against an authPersistNonNTLM=false server has been silently broken since well before v7.4.9. The way to make them work was by calling $connection->reset() before a request. This broke due to reset() unset the share handle and the share owned the connection cache via CURL_LOCK_DATA_CONNECT. #64046 correctly removed that share to honour max_host_connections, but as a side-effect it removed the resetting of the connection needed for the NTLM requests.

Fix: in CurlResponse::perform(), detect a 401 with an NTLM WWW-Authenticate arriving on a reused connection (CURLINFO_NUM_CONNECTS == 0), close the deauthenticated socket, retry on a fresh connection, and record the origin. Subsequent requests to a recorded origin set CURLOPT_FRESH_CONNECT and CURLOPT_FORBID_REUSE at setup time so they skip the pool.

Tests for the setup-time and negative cases. Cannot cover the full reused-connection because the cli-server forces connection: close, which doesnt allow me to recreate the issue in the test.

Considered alternative: making CurlClientState::reset() close NTLM-authenticated connections, which would restore the pre-#64046 side-effect for consumers that call reset() between requests, but this seemed like a sturdier fix.

My current local fix to get it working again in 7.4.9 (which I can remove again after this PR) is adding the CURLOPT_FORBID_REUSE to my framework.yml config.

  framework:
      http_client:
          scoped_clients:
              my_ntlm.client:
                  base_uri: '%env(NTLM_BASE_URI)%'
                  auth_ntlm: '%env(NTLM_AUTH_KEY)%'
                  extra:
                      curl:
                          !php/const CURLOPT_FORBID_REUSE: true

@carsonbot carsonbot added this to the 6.4 milestone May 23, 2026
@carsonbot carsonbot changed the title ntlm regression on authPersistNonNTLM=false connections with reset() ntlm regression on authPersistNonNTLM=false connections with reset() May 23, 2026
@carsonbot carsonbot changed the title ntlm regression on authPersistNonNTLM=false connections with reset() [HttpClient] ntlm regression on authPersistNonNTLM=false connections with reset() May 23, 2026
@symfony symfony deleted a comment from carsonbot May 23, 2026
…where reset no longer discards the connection.
@nicolas-grekas
Copy link
Copy Markdown
Member

Thank you @Dooij, and congrats for your very first PR!

@nicolas-grekas nicolas-grekas merged commit 6643bdb into symfony:6.4 May 23, 2026
6 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.