Skip to content

Fix async Rest5 client withHeaders() not seeing ThreadLocal auth (#3300)#3305

Open
014-code wants to merge 1 commit into
spring-projects:mainfrom
014-code:3300-rest5-withheaders-threadlocal
Open

Fix async Rest5 client withHeaders() not seeing ThreadLocal auth (#3300)#3305
014-code wants to merge 1 commit into
spring-projects:mainfrom
014-code:3300-rest5-withheaders-threadlocal

Conversation

@014-code

@014-code 014-code commented Jul 5, 2026

Copy link
Copy Markdown

Summary

ClientConfiguration.withHeaders() configured with a Supplier<HttpHeaders> that reads a ThreadLocal (typical for Spring Security's SecurityContextHolder) silently fails on the new Rest5 async client.

Root cause

Rest5Clients.getRest5ClientBuilder(...) registered the headers supplier as an HttpRequestInterceptor on the HttpAsyncClientBuilder. In HC 5, request interceptors run on the I/O reactor thread, so the supplier executes off the calling thread and cannot see any ThreadLocal set by the caller.

The legacy RestClients (HC 4) used addInterceptorLast, which runs synchronously on the calling thread, so the bug does not manifest there.

Fix

Replace addRequestInterceptorFirst with addExecInterceptorFirst. The AsyncExecChain handler executes on the calling thread before the request is dispatched asynchronously, so the supplier now sees the caller's ThreadLocal.

While there, fix a pre-existing typo: " Content-Type" had a leading space so the override never matched the real Content-Type header.

Regression test

RestClientsTest.shouldConfigureClientAndSetAllRequiredHeaders now:

  1. Reads a ThreadLocal inside the headers supplier and exposes the value as a thread header.
  2. Sets the ThreadLocal on the calling thread before invoking client.ping().
  3. Verifies that wireMock received the thread: local header.
  4. Asserts the supplier thread name equals the calling thread name, locking in the root cause against future regressions that would move the supplier back onto the async I/O thread.

The test runs against all three client factories (ElasticsearchRestClient / ElasticsearchRest5Client / ReactiveElasticsearchClient (ELC based)). Only the Rest5 factory exercises the changed code; the others serve as a regression net.

#3300

…ing-projects#3300)

Rest5Clients registered the ClientConfiguration.withHeaders() supplier as
an HttpRequestInterceptor on the Apache HttpAsyncClientBuilder. Since HC 5
runs request interceptors on the I/O reactor thread, the supplier executes
off the calling thread and cannot see a ThreadLocal (e.g. Spring Security's
SecurityContextHolder). With basic auth or tokens held in a ThreadLocal,
requests are sent without the expected Authorization header.

Switch the registration to addExecInterceptorFirst so the supplier runs on
the AsyncExecChain, which executes in the calling thread before the request
is dispatched asynchronously.

Also fix a pre-existing typo: " Content-Type" had a leading space so the
override never matched the real Content-Type header.

Closes spring-projects#3300

Signed-off-by: 014-code <2402143478@qq.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants