Skip to content

RestClient advanced configuration #48479

@yanivnahoum

Description

@yanivnahoum

In Spring Boot 3.5.x, when using RestClient with Apache HttpClient5, there are typically some configurations required as the defaults are not production-ready:

  1. Setting connect/read timeouts
  2. Setting the connection pool size: max-per-route/max-total (defaults are only 5/25)

Now in case I have 2 (or more) http clients in my app, I can configure my timeouts globally, as well as my factory.

spring:
  http:
    client:
      factory: http-components
      connect-timeout: 5s
      read-timeout: 10s

In order to customize the Apache HttpClient5 connection pool, I can expose a ClientHttpRequestFactoryBuilderCustomizer bean, or a HttpComponentsClientHttpRequestFactoryBuilder bean:

@Bean
ClientHttpRequestFactoryBuilderCustomizer<HttpComponentsClientHttpRequestFactoryBuilder> customizer() {
    return builder -> builder.withConnectionManagerCustomizer(this::customizeConnectionPool);
}

private void customizeConnectionPool(PoolingHttpClientConnectionManagerBuilder pool) {
    pool.setMaxConnPerRoute(100);
    pool.setMaxConnTotal(400);
}

I would inject a RestClient.Builder into the constructor of each of my 2 client classes, and configure each one according to its special needs:

# In SomeClient1.java
private final RestClient restClient;

public SomeClient1(RestClient.Builder builder, SomeClient1Properties properties) {
    this.restClient = builder.baseUrl(properties.baseUrl()).build();
}
# In SomeClient2.java
private final RestClient restClient;

public SomeClient2(RestClient.Builder builder, SomeClient2Properties properties) {
    this.restClient = builder.baseUrl(properties.baseUrl()).build();
}

But now say SomeClient2 needs a different read timeout since the global default is too short for the specific service it's calling. I can adjust it as follows:

# In SomeClient2.java
private final RestClient restClient;

public SomeClient2(RestClient.Builder builder, SomeClient2Properties properties) {
    ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.withReadTimeout(Duration.ofMinutes(1));
    ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.httpComponents().build(settings);
    this.restClient = builder.baseUrl(properties.baseUrl())
            .requestFactory(requestFactory)
            .build();
}

But this change is not additive - it replaces the ClientHttpRequestFactory created by the auto-configured ClientHttpRequestFactoryBuilder that included my ClientHttpRequestFactoryBuilderCustomizer.

Finally the question: was the EAGER connection between the ClientHttpRequestFactorySettings and the ClientHttpRequestFactoryBuilder done intentionally?

Could the 2 concerns have been separated and used only in the RestClient.build() phase to allow overriding one but not the other?

private final RestClient restClient;

public SomeClient2(RestClient.Builder builder, SomeClient2Properties properties) {
    ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.withReadTimeout(Duration.ofMinutes(1));
    // pseudo-code - override settings but use auto-configured ClientHttpRequestFactoryBuilder
    this.restClient = builder.baseUrl(properties.baseUrl())
            .requestSettings(settings)
            .build();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions