Skip to content

Add setExecutor to JdkClientHttpRequestFactory to allow safe customization of blocking adapter threads #35997

@NYgomets

Description

@NYgomets

Overview JdkClientHttpRequestFactory currently defaults to using SimpleAsyncTaskExecutor when the provided java.net.http.HttpClient does not expose an executor. Since SimpleAsyncTaskExecutor creates a new platform thread per task, this can lead to thread exhaustion under high concurrency, especially for streaming requests or responses.

Unlike ReactorClientHttpRequestFactory (which introduced setExecutor in 6.2.13), JdkClientHttpRequestFactory provides no way to override the executor after instantiation without rebuilding the underlying HttpClient.

Problem Description When users create a standard shared HttpClient:

HttpClient sharedClient = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build(); // No explicit executor set

The constructor of JdkClientHttpRequestFactory executes:

this.executor = httpClient.executor().orElseGet(SimpleAsyncTaskExecutor::new);

Since HttpClient.executor() often returns Optional.empty() when using the default internal pool, Spring falls back to SimpleAsyncTaskExecutor.

This causes three significant issues:

  1. Thread Explosion Risk: SimpleAsyncTaskExecutor creates an unbounded number of threads. During blocking I/O (e.g., large file uploads/downloads, streaming responses), this can lead to excessive thread creation and resource exhaustion.

  2. No Customization Point: Users cannot assign a bounded thread pool, a VirtualThreadPerTaskExecutor, or a shared application-level executor without rebuilding the entire HttpClient.

  3. Inconsistency Across Client Adapters: ReactorClientHttpRequestFactory provides an explicit setExecutor hook, while JdkClientHttpRequestFactory does not.

Proposed Solution Introduce a method such as:

public void setExecutor(Executor executor) {
    Assert.notNull(executor, "Executor must not be null");
    this.executor = executor;
}

This executor would be used for the blocking I/O adapter layer and would not interfere with the internal executor of the underlying JDK HttpClient.

Desired Usage

@Bean
public ClientHttpRequestFactory jdkClientHttpRequestFactory(HttpClient sharedClient) {
    JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(sharedClient);

    // Provide a safe executor (e.g., Virtual Threads or Bounded Pool)
    factory.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    // or: factory.setExecutor(new ThreadPoolTaskExecutor());

    return factory;
}

Backwards Compatibility This change preserves current behavior:

  • If no executor is set, SimpleAsyncTaskExecutor continues to be the default fallback.

  • Existing applications experience no change in behavior.

Additional Suggestion A Javadoc note clarifying that the default fallback is SimpleAsyncTaskExecutor (unbounded thread creation) would help guide users toward safer production configurations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: webIssues in web modules (web, webmvc, webflux, websocket)status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions