Skip to content

Commit

Permalink
Allow customising HC5 RequestConfig in EurekaClientHttpRequestFactory…
Browse files Browse the repository at this point in the history
…Supplier (#4394)
  • Loading branch information
OlgaMaciaszek authored Jan 16, 2025
1 parent bd38ceb commit d1eb029
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 41 deletions.
15 changes: 15 additions & 0 deletions docs/modules/ROOT/pages/spring-cloud-netflix.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ eureka:
socket-timeout: 10000
----

You can also customise the `RequestConfig` for the underlying Apache HttpClient 5 by creating a bean of type `EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer`:

[source,java,indent=0]
----
@Configuration
public class RestClientConfiguration {
@Bean
EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer requestConfigCustomizer() {
return builder -> builder.setProtocolUpgradeEnabled(false);
}
}
----

=== Status Page and Health Indicator

The status page and health indicators for a Eureka instance default to `/info` and `/health` respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2024 the original author or authors.
* Copyright 2018-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,8 @@

package org.springframework.cloud.netflix.eureka;

import java.util.Collections;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -64,8 +66,8 @@ public RestTemplateTransportClientFactories forceRestTemplateTransportClientFact

@Bean
public RestTemplateDiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
return new RestTemplateDiscoveryClientOptionalArgs(
new DefaultEurekaClientHttpRequestFactorySupplier(new RestTemplateTimeoutProperties()), null);
return new RestTemplateDiscoveryClientOptionalArgs(new DefaultEurekaClientHttpRequestFactorySupplier(
new RestTemplateTimeoutProperties(), Collections.emptySet()));
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2024 the original author or authors.
* Copyright 2017-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,7 @@

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Set;

import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import com.netflix.discovery.shared.transport.jersey.TransportClientFactories;
Expand Down Expand Up @@ -94,8 +95,10 @@ static class RestTemplateConfiguration {
@Bean
@ConditionalOnMissingBean
EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier(
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restTemplateTimeoutProperties);
RestTemplateTimeoutProperties restTemplateTimeoutProperties,
Set<EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer> requestConfigCustomizers) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restTemplateTimeoutProperties,
requestConfigCustomizers);
}

@Bean
Expand Down Expand Up @@ -189,8 +192,10 @@ protected static class RestClientConfiguration {
@Bean
@ConditionalOnMissingBean
EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier(
RestClientTimeoutProperties restClientTimeoutProperties) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restClientTimeoutProperties);
RestClientTimeoutProperties restClientTimeoutProperties,
Set<EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer> requestConfigCustomizers) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restClientTimeoutProperties,
requestConfigCustomizers);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2024 the original author or authors.
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,8 @@

package org.springframework.cloud.netflix.eureka.config;

import java.util.Set;

import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.shared.transport.EurekaHttpClient;

Expand Down Expand Up @@ -54,7 +56,7 @@
import org.springframework.web.reactive.function.client.WebClient;

/**
* Bootstrap configuration for config client that wants to lookup the config server via
* Bootstrap configuration for config client that wants to look the config server up via
* discovery.
*
* @author Dave Syer
Expand Down Expand Up @@ -103,8 +105,10 @@ public RestTemplateEurekaHttpClient configDiscoveryRestTemplateEurekaHttpClient(
@Bean
@ConditionalOnMissingBean
EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier(
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restTemplateTimeoutProperties);
RestTemplateTimeoutProperties restTemplateTimeoutProperties,
Set<EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer> requestConfigCustomizers) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restTemplateTimeoutProperties,
requestConfigCustomizers);
}

/**
Expand Down Expand Up @@ -174,8 +178,10 @@ public RestClientEurekaHttpClient configDiscoveryRestClientEurekaHttpClient(Eure
@Bean
@ConditionalOnMissingBean
EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier(
RestClientTimeoutProperties restClientTimeoutProperties) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restClientTimeoutProperties);
RestClientTimeoutProperties restClientTimeoutProperties,
Set<EurekaClientHttpRequestFactorySupplier.RequestConfigCustomizer> requestConfigCustomizers) {
return new DefaultEurekaClientHttpRequestFactorySupplier(restClientTimeoutProperties,
requestConfigCustomizers);
}

static class OnRestClientPresentAndEnabledCondition extends AllNestedConditions {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2024 the original author or authors.
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -61,7 +61,8 @@ public void initialize(BootstrapRegistry registry) {
EurekaHttpClient httpClient = new RestClientTransportClientFactory(
context.getOrElse(TlsProperties.class, null),
context.getOrElse(EurekaClientHttpRequestFactorySupplier.class,
new DefaultEurekaClientHttpRequestFactorySupplier(new RestClientTimeoutProperties())))
new DefaultEurekaClientHttpRequestFactorySupplier(new RestClientTimeoutProperties(),
Collections.emptySet())))
.newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, getPropertyResolver(context)));
return new EurekaConfigServerInstanceProvider(httpClient, config)::getInstances;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2024 the original author or authors.
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,11 +16,14 @@

package org.springframework.cloud.netflix.eureka.http;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
Expand Down Expand Up @@ -50,9 +53,12 @@ public class DefaultEurekaClientHttpRequestFactorySupplier implements EurekaClie

private final TimeoutProperties timeoutProperties;

// TODO: switch to final after removing deprecated interfaces
private Set<RequestConfigCustomizer> requestConfigCustomizers = Collections.emptySet();

/**
* @deprecated in favour of
* {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties)}
* {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties, Set)}
*/
@Deprecated(forRemoval = true)
public DefaultEurekaClientHttpRequestFactorySupplier() {
Expand All @@ -61,27 +67,36 @@ public DefaultEurekaClientHttpRequestFactorySupplier() {

/**
* @deprecated in favour of
* {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties)}
* {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties, Set)}
*/
@Deprecated(forRemoval = true)
public DefaultEurekaClientHttpRequestFactorySupplier(RestTemplateTimeoutProperties timeoutProperties) {
this.timeoutProperties = timeoutProperties;
}

/**
* @deprecated in favour of
* {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties, Set)}
*/
@Deprecated(forRemoval = true)
public DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties timeoutProperties) {
this.timeoutProperties = timeoutProperties;
}

public DefaultEurekaClientHttpRequestFactorySupplier(TimeoutProperties timeoutProperties,
Set<RequestConfigCustomizer> requestConfigCustomizers) {
this.timeoutProperties = timeoutProperties;
this.requestConfigCustomizers = requestConfigCustomizers;
}

@Override
public ClientHttpRequestFactory get(SSLContext sslContext, @Nullable HostnameVerifier hostnameVerifier) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
if (sslContext != null || hostnameVerifier != null || timeoutProperties != null) {
httpClientBuilder
.setConnectionManager(buildConnectionManager(sslContext, hostnameVerifier, timeoutProperties));
}
if (timeoutProperties != null) {
httpClientBuilder.setDefaultRequestConfig(buildRequestConfig());
}
httpClientBuilder.setDefaultRequestConfig(buildRequestConfig());

CloseableHttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
Expand All @@ -90,7 +105,7 @@ public ClientHttpRequestFactory get(SSLContext sslContext, @Nullable HostnameVer
}

private HttpClientConnectionManager buildConnectionManager(SSLContext sslContext, HostnameVerifier hostnameVerifier,
TimeoutProperties restTemplateTimeoutProperties) {
TimeoutProperties timeoutProperties) {
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
.create();
SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder
Expand All @@ -102,20 +117,25 @@ private HttpClientConnectionManager buildConnectionManager(SSLContext sslContext
sslConnectionSocketFactoryBuilder.setHostnameVerifier(hostnameVerifier);
}
connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactoryBuilder.build());
if (restTemplateTimeoutProperties != null) {
if (timeoutProperties != null) {
connectionManagerBuilder.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.of(restTemplateTimeoutProperties.getSocketTimeout(), TimeUnit.MILLISECONDS))
.setSoTimeout(Timeout.of(timeoutProperties.getSocketTimeout(), TimeUnit.MILLISECONDS))
.build());
connectionManagerBuilder.setDefaultConnectionConfig(ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(timeoutProperties.getConnectTimeout(), TimeUnit.MILLISECONDS))
.build());
}
return connectionManagerBuilder.build();
}

private RequestConfig buildRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(Timeout.of(timeoutProperties.getConnectTimeout(), TimeUnit.MILLISECONDS))
.setConnectionRequestTimeout(
Timeout.of(timeoutProperties.getConnectRequestTimeout(), TimeUnit.MILLISECONDS))
.build();
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
if (timeoutProperties != null) {
requestConfigBuilder.setConnectionRequestTimeout(
Timeout.of(timeoutProperties.getConnectRequestTimeout(), TimeUnit.MILLISECONDS));
}
requestConfigCustomizers.forEach(customizer -> customizer.customize(requestConfigBuilder));
return requestConfigBuilder.build();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 the original author or authors.
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,8 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import org.apache.hc.client5.http.config.RequestConfig;

import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.lang.Nullable;

Expand All @@ -38,4 +40,17 @@ public interface EurekaClientHttpRequestFactorySupplier {
*/
ClientHttpRequestFactory get(SSLContext sslContext, @Nullable HostnameVerifier hostnameVerifier);

/**
* Allows customising the {@link RequestConfig} of the underlying Apache HC5 instance.
*
* @author Olga Maciaszek-Sharma
* @since 4.2.1
*/
@FunctionalInterface
interface RequestConfigCustomizer {

void customize(RequestConfig.Builder builder);

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,6 +31,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
Expand All @@ -45,6 +46,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
Expand All @@ -59,6 +61,7 @@
*
* @author Daniel Lavoie
* @author Wonchul Heo
* @author Olga Maciaszek-Sharma
*/
@Configuration(proxyBeanMethods = false)
@RestController
Expand Down Expand Up @@ -172,7 +175,11 @@ public Applications getApplications(@PathVariable(required = false) String addre
}

@GetMapping("/apps/{appName}")
public Application getApplication(@PathVariable String appName) {
public Application getApplication(@PathVariable String appName, @RequestHeader HttpHeaders headers) {
// Used to verify that RequestConfig customizer has taken effect
if (appName.equals("upgrade") && !headers.containsKey("upgrade")) {
throw new RuntimeException("No upgrade header found");
}
return new Application();
}

Expand Down
Loading

0 comments on commit d1eb029

Please sign in to comment.