diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml
new file mode 100644
index 00000000..54472f7b
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ httpclient
+ jar
+
+ Spring Cloud Gateway HttpClient Integration Test
+ Spring Cloud Gateway HttpClient Integration Test
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-gateway-integration-tests
+ 4.3.0-SNAPSHOT
+ ..
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway-server-webmvc
+
+
+
+ org.springframework.retry
+ spring-retry
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java b/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java
new file mode 100644
index 00000000..6e198797
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.gateway.tests.httpclient;
+
+import java.time.Duration;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hc.core5.util.Timeout;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
+import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.function.RouterFunction;
+import org.springframework.web.servlet.function.ServerResponse;
+
+import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
+import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
+import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
+import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
+import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
+
+/**
+ * @author jiangyuan
+ */
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@LoadBalancerClient(name = "myservice", configuration = MyServiceConf.class)
+public class HttpClientApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(HttpClientApplication.class, args);
+ }
+
+ @Bean
+ public HttpComponentsClientHttpRequestFactoryBuilder httpComponentsClientHttpRequestFactoryBuilder() {
+ return ClientHttpRequestFactoryBuilder.httpComponents()
+ .withConnectionManagerCustomizer(builder -> builder.setMaxConnTotal(2).setMaxConnPerRoute(2))
+ .withDefaultRequestConfigCustomizer(
+ c -> c.setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000))));
+ }
+
+ @Bean
+ public RouterFunction gatewayRouterFunctionsRetry() {
+ return route("test-retry").GET("/retry", http())
+ .filter(lb("myservice"))
+ .filter(prefixPath("/do"))
+ .filter(retry(3))
+ .build();
+ }
+
+ @RestController
+ protected static class RetryController {
+
+ Log log = LogFactory.getLog(getClass());
+
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+
+ @GetMapping("/do/retry")
+ public ResponseEntity retry(@RequestParam("key") String key,
+ @RequestParam(name = "count", defaultValue = "3") int count,
+ @RequestParam(name = "failStatus", required = false) Integer failStatus) {
+ AtomicInteger num = map.computeIfAbsent(key, s -> new AtomicInteger());
+ int i = num.incrementAndGet();
+ log.warn("Retry count: " + i);
+ String body = String.valueOf(i);
+ if (i < count) {
+ HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
+ if (failStatus != null) {
+ httpStatus = HttpStatus.resolve(failStatus);
+ }
+ return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken");
+ }
+ return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body);
+ }
+
+ }
+
+}
+
+class MyServiceConf {
+
+ @Value("${local.server.port}")
+ private int port = 0;
+
+ @Bean
+ public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
+ return ServiceInstanceListSuppliers.from("myservice",
+ new DefaultServiceInstance("myservice-1", "myservice", "localhost", port, false));
+ }
+
+}
diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml
new file mode 100644
index 00000000..106caafa
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+logging:
+ level:
+ org.springframework.cloud.gateway: TRACE
diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java b/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java
new file mode 100644
index 00000000..13269d41
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.gateway.tests.httpclient;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+/**
+ * @author jiangyuan
+ */
+@SpringBootTest(classes = HttpClientApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@DirtiesContext
+public class HttpClientApplicationTests {
+
+ @LocalServerPort
+ private int port;
+
+ @Test
+ public void retryWorks() {
+ WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build();
+ client.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
+ }
+
+}
diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml
index 8fc43a9b..cab51af8 100644
--- a/spring-cloud-gateway-integration-tests/pom.xml
+++ b/spring-cloud-gateway-integration-tests/pom.xml
@@ -24,6 +24,7 @@
grpc
http2
mvc-failure-analyzer
+ httpclient
diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java
index be2bfcba..edae11e4 100644
--- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java
+++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java
@@ -16,41 +16,30 @@
package org.springframework.cloud.gateway.server.mvc.filter;
-import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.apache.hc.core5.util.Timeout;
import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
-import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
-import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchange;
-import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchangeHandlerFunction;
-import org.springframework.cloud.gateway.server.mvc.handler.RestClientProxyExchange;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver;
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
-import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
@@ -58,13 +47,11 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.client.RestClient;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody;
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
-import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setPath;
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -106,17 +93,6 @@ public class RetryFilterFunctionTests {
.isEqualTo("3");
}
- @Test
- public void retryWorksWithHttpComponentsClient() {
- restClient.get()
- .uri("/retrywithhttpcomponentsclient?key=retryWorksWithHttpComponentsClient")
- .exchange()
- .expectStatus()
- .isOk()
- .expectBody(String.class)
- .isEqualTo("3");
- }
-
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
@@ -134,41 +110,6 @@ public class RetryFilterFunctionTests {
// @formatter:on
}
- @Bean
- public RouterFunction gatewayRouterFunctionsRetryWithHttpComponentsClient(
- GatewayMvcProperties properties,
- ObjectProvider requestHttpHeadersFilters,
- ObjectProvider responseHttpHeadersFilters,
- ApplicationContext applicationContext) {
-
- // build httpComponents client factory
- ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryBuilder.httpComponents()
- .withConnectionManagerCustomizer(builder -> builder.setMaxConnTotal(2).setMaxConnPerRoute(2))
- .withDefaultRequestConfigCustomizer(
- c -> c.setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000))))
- .build();
-
- // build proxyExchange use httpComponents
- RestClient.Builder restClientBuilder = RestClient.builder();
- restClientBuilder.requestFactory(clientHttpRequestFactory);
- ProxyExchange proxyExchange = new RestClientProxyExchange(restClientBuilder.build(), properties);
-
- // build handler function use httpComponents
- ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange,
- requestHttpHeadersFilters, responseHttpHeadersFilters);
- function.onApplicationEvent(new ContextRefreshedEvent(applicationContext));
-
- // @formatter:off
- return route("testretrywithhttpcomponentsclient")
- .GET("/retrywithhttpcomponentsclient", function)
- .before(new LocalServerPortUriResolver())
- .filter(retry(3))
- .filter(setPath("/retry"))
- .filter(prefixPath("/do"))
- .build();
- // @formatter:on
- }
-
@Bean
public RouterFunction gatewayRouterFunctionsRetryBody() {
// @formatter:off
diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml
index 60537dfc..9bc1759e 100644
--- a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml
+++ b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml
@@ -6,7 +6,4 @@ logging:
org.springframework.retry: TRACE
spring:
mvc:
- log-request-details: true
- http:
- client:
- factory: REACTOR
\ No newline at end of file
+ log-request-details: true
\ No newline at end of file