From 7f6fcddc9ffedbd13af5c31e45ea500795fba7d4 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Mon, 17 Mar 2025 21:10:26 +0800 Subject: [PATCH 1/7] 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 272b1e6c..909aa74c 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 abstract class RetryFilterFunctions { if (config.isCacheBody()) { MvcUtils.getOrCacheBody(request); } + reset(request); ServerResponse serverResponse = next.handle(request); if (isRetryableStatusCode(serverResponse.statusCode(), config) @@ -86,6 +88,20 @@ public abstract class RetryFilterFunctions { }); } + /** + * 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 a261d17b4e594092d9f2234af975bf7fe947c01c Mon Sep 17 00:00:00 2001 From: joecqupt Date: Mon, 17 Mar 2025 22:47:18 +0800 Subject: [PATCH 2/7] 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 909aa74c..9c421727 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 @@ public abstract class RetryFilterFunctions { }); } - /** - * 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 7b79e181f2c19ab5f6837191fff5d10aab316644 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Mon, 31 Mar 2025 13:09:37 +0800 Subject: [PATCH 3/7] 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 05c073ed..27bfa1e1 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 9c421727..4de07b1d 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.core.NestedRuntimeException; 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 @@ public abstract class RetryFilterFunctions { } 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 5c17dd2d..db1685d9 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 class ClientHttpRequestFactoryProxyExchange extends AbstractProxyExchange 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 46e9d032..315a061f 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 @@ public class RestClientProxyExchange extends AbstractProxyExchange { 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 7c9d43c0192e76cb94fe5be4edb66ca4e99cb4d5 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Wed, 2 Apr 2025 22:22:31 +0800 Subject: [PATCH 4/7] add unit-test Signed-off-by: jiangyuan --- .../mvc/filter/RetryFilterFunctionTests.java | 60 +++++++++++++++++++ .../src/test/resources/application.yml | 5 +- 2 files changed, 64 insertions(+), 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 edae11e4..430f3934 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.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.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 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) @@ -110,6 +135,41 @@ 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 9bc1759e..60537dfc 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 5247da10f269a57fd88b2d6d11205ce144fe8545 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Thu, 3 Apr 2025 11:10:26 +0800 Subject: [PATCH 5/7] 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 430f3934..be2bfcba 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.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; From bcf26e8df34f2d8099d731ae30340d168dec482c Mon Sep 17 00:00:00 2001 From: joecqupt Date: Mon, 21 Apr 2025 01:49:18 +0800 Subject: [PATCH 6/7] 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 + .../mvc/filter/RetryFilterFunctionTests.java | 59 --------- .../src/test/resources/application.yml | 5 +- 7 files changed, 245 insertions(+), 63 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 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 From 4ff93c591c2dfb6a70043e6392fa3575a67b841e Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Thu, 15 May 2025 14:53:21 +0800 Subject: [PATCH 7/7] update base branch to 4.1.x Signed-off-by: jiangyuan --- .../httpclient/pom.xml | 4 +-- .../httpclient/HttpClientApplication.java | 25 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml index 54472f7b..08178c87 100644 --- a/spring-cloud-gateway-integration-tests/httpclient/pom.xml +++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.3.0-SNAPSHOT + 4.1.8-SNAPSHOT .. @@ -34,7 +34,7 @@ org.springframework.cloud - spring-cloud-starter-gateway-server-webmvc + spring-cloud-starter-gateway-mvc 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 index 6e198797..9390e611 100644 --- 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 @@ -22,14 +22,16 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +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.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; 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; @@ -37,6 +39,7 @@ import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSupplie import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -62,11 +65,19 @@ public class HttpClientApplication { } @Bean - public HttpComponentsClientHttpRequestFactoryBuilder httpComponentsClientHttpRequestFactoryBuilder() { - return ClientHttpRequestFactoryBuilder.httpComponents() - .withConnectionManagerCustomizer(builder -> builder.setMaxConnTotal(2).setMaxConnPerRoute(2)) - .withDefaultRequestConfigCustomizer( - c -> c.setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000)))); + public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(2); + connectionManager.setDefaultMaxPerRoute(2); + + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig( + RequestConfig.custom().setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000))).build()) + .build(); + + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + return factory; } @Bean