From ff5edf22a0c68f2a34d497f3c9012229306ba794 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Mon, 17 Mar 2025 21:10:26 +0800 Subject: [PATCH 1/6] fix httpclient5.x connection leak Signed-off-by: jiangyuan --- .../server/mvc/filter/RetryFilterFunctions.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java index 272b1e6ce8..909aa74cdc 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.server.mvc.filter; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -75,6 +76,7 @@ public static HandlerFilterFunction retry(RetryC if (config.isCacheBody()) { MvcUtils.getOrCacheBody(request); } + reset(request); ServerResponse serverResponse = next.handle(request); if (isRetryableStatusCode(serverResponse.statusCode(), config) @@ -86,6 +88,20 @@ && isRetryableMethod(request.method(), config)) { }); } + /** + * reset attribute + * + * @param request + * @throws IOException + */ + private static void reset(ServerRequest request) throws IOException { + InputStream inputStream = MvcUtils.getAttribute(request, MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR); + if (inputStream != null) { + inputStream.close(); + MvcUtils.putAttribute(request, MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, null); + } + } + private static boolean isRetryableStatusCode(HttpStatusCode httpStatus, RetryConfig config) { return config.getSeries().stream().anyMatch(series -> HttpStatus.Series.resolve(httpStatus.value()) == series); } From 6ce0e1d542d33b4ca9fbb33e50bef75ec8d1f7e2 Mon Sep 17 00:00:00 2001 From: joecqupt Date: Mon, 17 Mar 2025 22:47:18 +0800 Subject: [PATCH 2/6] fix format Signed-off-by: joecqupt Signed-off-by: jiangyuan --- .../gateway/server/mvc/filter/RetryFilterFunctions.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java index 909aa74cdc..9c42172700 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java @@ -88,12 +88,6 @@ && isRetryableMethod(request.method(), config)) { }); } - /** - * reset attribute - * - * @param request - * @throws IOException - */ private static void reset(ServerRequest request) throws IOException { InputStream inputStream = MvcUtils.getAttribute(request, MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR); if (inputStream != null) { From aeffc5e555b622ed703eeb07b4af22f5347d4064 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Mon, 31 Mar 2025 13:09:37 +0800 Subject: [PATCH 3/6] add client response attrs Signed-off-by: jiangyuan test ci build --- .../cloud/gateway/server/mvc/common/MvcUtils.java | 5 +++++ .../server/mvc/filter/RetryFilterFunctions.java | 10 +++++----- .../handler/ClientHttpRequestFactoryProxyExchange.java | 1 + .../server/mvc/handler/RestClientProxyExchange.java | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 05c073ed2d..27bfa1e1f2 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -63,6 +63,11 @@ public abstract class MvcUtils { */ public static final String CLIENT_RESPONSE_INPUT_STREAM_ATTR = qualify("cachedClientResponseBody"); + /** + * Client response key. + */ + public static final String CLIENT_RESPONSE_ATTR = qualify("cachedClientResponse"); + /** * CircuitBreaker execution exception attribute name. */ diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java index 9c42172700..4de07b1d82 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java @@ -17,7 +17,6 @@ package org.springframework.cloud.gateway.server.mvc.filter; import java.io.IOException; -import java.io.InputStream; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -34,6 +33,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryPolicy; import org.springframework.retry.policy.CompositeRetryPolicy; @@ -89,10 +89,10 @@ && isRetryableMethod(request.method(), config)) { } private static void reset(ServerRequest request) throws IOException { - InputStream inputStream = MvcUtils.getAttribute(request, MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR); - if (inputStream != null) { - inputStream.close(); - MvcUtils.putAttribute(request, MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, null); + ClientHttpResponse clientHttpResponse = MvcUtils.getAttribute(request, MvcUtils.CLIENT_RESPONSE_ATTR); + if (clientHttpResponse != null) { + clientHttpResponse.close(); + MvcUtils.putAttribute(request, MvcUtils.CLIENT_RESPONSE_ATTR, null); } } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ClientHttpRequestFactoryProxyExchange.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ClientHttpRequestFactoryProxyExchange.java index 5c17dd2df9..db1685d9a7 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ClientHttpRequestFactoryProxyExchange.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ClientHttpRequestFactoryProxyExchange.java @@ -56,6 +56,7 @@ public ServerResponse exchange(Request request) { InputStream body = clientHttpResponse.getBody(); // put the body input stream in a request attribute so filters can read it. MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body); + MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_ATTR, clientHttpResponse); ServerResponse serverResponse = GatewayServerResponse.status(clientHttpResponse.getStatusCode()) .build((req, httpServletResponse) -> { try (clientHttpResponse) { diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java index 46e9d0326b..315a061f72 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java @@ -72,6 +72,7 @@ private ServerResponse doExchange(Request request, ClientHttpResponse clientResp InputStream body = clientResponse.getBody(); // put the body input stream in a request attribute so filters can read it. MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body); + MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_ATTR, clientResponse); ServerResponse serverResponse = GatewayServerResponse.status(clientResponse.getStatusCode()) .build((req, httpServletResponse) -> { try (clientResponse) { From 007667705e66021b6d2f5bdb59f6b58114aab346 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Wed, 2 Apr 2025 22:22:31 +0800 Subject: [PATCH 4/6] add unit-test Signed-off-by: jiangyuan --- spring-cloud-gateway-server-mvc/pom.xml | 5 ++ ...atewayServerMvcAutoConfigurationTests.java | 9 +-- .../mvc/filter/RetryFilterFunctionTests.java | 60 +++++++++++++++++++ .../src/test/resources/application.yml | 5 +- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index e9f6b80c1a..1da12e2dd6 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -135,5 +135,10 @@ rabbitmq test + + org.apache.httpcomponents.client5 + httpclient5 + test + diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java index 7eb5640525..bd1dc400b2 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java @@ -33,7 +33,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; -import org.springframework.boot.http.client.SimpleClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ReactorClientHttpRequestFactoryBuilder; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.gateway.server.mvc.filter.FormFilter; import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter; @@ -47,6 +47,7 @@ import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.autoconfigure.http.client.HttpClientProperties.Factory.REACTOR; public class GatewayServerMvcAutoConfigurationTests { @@ -151,7 +152,7 @@ void gatewayHttpClientPropertiesWork() { assertThat(properties.getConnectTimeout()).hasSeconds(1); assertThat(properties.getReadTimeout()).hasSeconds(2); assertThat(properties.getSsl().getBundle()).isEqualTo("mybundle"); - assertThat(properties.getFactory()).isNull(); + assertThat(properties.getFactory()).isEqualTo(REACTOR); assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(2)); assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(1)); assertThat(settings.sslBundle()).isNotNull(); @@ -187,10 +188,10 @@ void bootHttpClientPropertiesWork() { @Test void settingHttpClientFactoryWorks() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) - .properties("spring.main.web-application-type=none", "spring.http.client.factory=simple") + .properties("spring.main.web-application-type=none") .run(); ClientHttpRequestFactoryBuilder builder = context.getBean(ClientHttpRequestFactoryBuilder.class); - assertThat(builder).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class); + assertThat(builder).isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class); } @SpringBootConfiguration 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 d93dfc5783..961151dca8 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,30 +16,41 @@ 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; @@ -47,11 +58,14 @@ 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.HandlerFunction; 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; @@ -93,6 +107,17 @@ public void retryBodyWorks() { .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) @@ -110,6 +135,41 @@ public RouterFunction gatewayRouterFunctionsRetry() { // @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 9bc1759e91..60537dfc48 100644 --- a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml @@ -6,4 +6,7 @@ logging: org.springframework.retry: TRACE spring: mvc: - log-request-details: true \ No newline at end of file + log-request-details: true + http: + client: + factory: REACTOR \ No newline at end of file From aa22085b876b48036036eb08df5390ef3e75029c Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Thu, 3 Apr 2025 11:10:26 +0800 Subject: [PATCH 5/6] fix format Signed-off-by: jiangyuan --- .../gateway/server/mvc/filter/RetryFilterFunctionTests.java | 1 - 1 file changed, 1 deletion(-) 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 961151dca8..317d9e5ad9 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 @@ -59,7 +59,6 @@ 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.HandlerFunction; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; From 44a9c819ee5eec704b8d691d4b47d61404ab372d Mon Sep 17 00:00:00 2001 From: joecqupt Date: Mon, 21 Apr 2025 01:49:18 +0800 Subject: [PATCH 6/6] add httpclint integration test module Signed-off-by: joecqupt --- .../httpclient/pom.xml | 77 +++++++++++ .../httpclient/HttpClientApplication.java | 121 ++++++++++++++++++ .../src/main/resources/application.yml | 3 + .../HttpClientApplicationTests.java | 42 ++++++ .../pom.xml | 1 + spring-cloud-gateway-server-mvc/pom.xml | 5 - ...atewayServerMvcAutoConfigurationTests.java | 9 +- .../mvc/filter/RetryFilterFunctionTests.java | 59 --------- .../src/test/resources/application.yml | 5 +- 9 files changed, 249 insertions(+), 73 deletions(-) create mode 100644 spring-cloud-gateway-integration-tests/httpclient/pom.xml create mode 100644 spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java create mode 100644 spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml create mode 100644 spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java 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 0000000000..54472f7b5a --- /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 0000000000..6e198797cf --- /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 0000000000..106caafa26 --- /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 0000000000..13269d418f --- /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 85b3c76dc0..6c0074b148 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/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index 1da12e2dd6..e9f6b80c1a 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -135,10 +135,5 @@ rabbitmq test - - org.apache.httpcomponents.client5 - httpclient5 - test - diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java index bd1dc400b2..7eb5640525 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfigurationTests.java @@ -33,7 +33,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; -import org.springframework.boot.http.client.ReactorClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.SimpleClientHttpRequestFactoryBuilder; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.gateway.server.mvc.filter.FormFilter; import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter; @@ -47,7 +47,6 @@ import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.boot.autoconfigure.http.client.HttpClientProperties.Factory.REACTOR; public class GatewayServerMvcAutoConfigurationTests { @@ -152,7 +151,7 @@ void gatewayHttpClientPropertiesWork() { assertThat(properties.getConnectTimeout()).hasSeconds(1); assertThat(properties.getReadTimeout()).hasSeconds(2); assertThat(properties.getSsl().getBundle()).isEqualTo("mybundle"); - assertThat(properties.getFactory()).isEqualTo(REACTOR); + assertThat(properties.getFactory()).isNull(); assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(2)); assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(1)); assertThat(settings.sslBundle()).isNotNull(); @@ -188,10 +187,10 @@ void bootHttpClientPropertiesWork() { @Test void settingHttpClientFactoryWorks() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) - .properties("spring.main.web-application-type=none") + .properties("spring.main.web-application-type=none", "spring.http.client.factory=simple") .run(); ClientHttpRequestFactoryBuilder builder = context.getBean(ClientHttpRequestFactoryBuilder.class); - assertThat(builder).isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class); + assertThat(builder).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class); } @SpringBootConfiguration 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 317d9e5ad9..d93dfc5783 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.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 void retryBodyWorks() { .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 RouterFunction gatewayRouterFunctionsRetry() { // @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 60537dfc48..9bc1759e91 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