Skip to content

Commit ccde721

Browse files
committed
Merge branch '4.2.x'
2 parents 7318535 + 1982315 commit ccde721

File tree

5 files changed

+212
-72
lines changed

5 files changed

+212
-72
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/retry.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The `Retry` filter supports the following parameters:
99
* `methods`: The HTTP methods that should be retried, represented by using `org.springframework.http.HttpMethod`.
1010
* `series`: The series of status codes to be retried, represented by using `org.springframework.http.HttpStatus.Series`.
1111
* `exceptions`: A list of thrown exceptions that should be retried.
12+
* `cacheBody`: A flag to signal if the request body should be cached. If set to `true`, the `adaptCacheBody` filter must be used to send the cached body downstream.
1213
//* `backoff`: The configured exponential backoff for the retries.
1314
//Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration.
1415
//If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`.
@@ -20,8 +21,11 @@ The following defaults are configured for `Retry` filter, if enabled:
2021
* `series`: 5XX series
2122
* `methods`: GET method
2223
* `exceptions`: `IOException`, `TimeoutException` and `RetryException`
24+
* `cacheBody`: `false`
2325
//* `backoff`: disabled
2426

27+
WARNING: Setting `cacheBody` to `true` causes the gateway to read the whole body into memory. This should be used with caution.
28+
2529
The following listing configures a Retry filter:
2630

2731
.application.yml
@@ -42,11 +46,14 @@ spring:
4246
retries: 3
4347
series: SERVER_ERROR
4448
methods: GET,POST
49+
cacheBody: true
50+
- name: AdaptCachedBody
4551
----
4652

4753
.GatewaySampleApplication.java
4854
[source,java]
4955
----
56+
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody;
5057
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
5158
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
5259
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -59,7 +66,8 @@ class RouteConfiguration {
5966
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
6067
return route("add_request_parameter_route")
6168
.route(host("*.retry.com"), http("https://example.org"))
62-
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
69+
.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST)).setCacheBody(true)))
70+
.filter(adaptCachedBody())
6371
.build();
6472
}
6573
}

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ public static ByteArrayInputStream cacheBody(ServerRequest request) {
116116
}
117117
}
118118

119+
public static ByteArrayInputStream getOrCacheBody(ServerRequest request) {
120+
ByteArrayInputStream body = getAttribute(request, MvcUtils.CACHED_REQUEST_BODY_ATTR);
121+
if (body != null) {
122+
return body;
123+
}
124+
return cacheBody(request);
125+
}
126+
119127
public static String expand(ServerRequest request, String template) {
120128
Assert.notNull(request, "request may not be null");
121129
Assert.notNull(template, "template may not be null");

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctions.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.function.Consumer;
2828

2929
import org.springframework.cloud.gateway.server.mvc.common.Configurable;
30+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
3031
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
3132
import org.springframework.core.NestedRuntimeException;
3233
import org.springframework.http.HttpMethod;
@@ -71,6 +72,9 @@ public static HandlerFilterFunction<ServerResponse, ServerResponse> retry(RetryC
7172
.setPolicies(Arrays.asList(simpleRetryPolicy, new HttpRetryPolicy(config)).toArray(new RetryPolicy[0]));
7273
RetryTemplate retryTemplate = retryTemplateBuilder.customPolicy(compositeRetryPolicy).build();
7374
return (request, next) -> retryTemplate.execute(context -> {
75+
if (config.isCacheBody()) {
76+
MvcUtils.getOrCacheBody(request);
77+
}
7478
ServerResponse serverResponse = next.handle(request);
7579

7680
if (isRetryableStatusCode(serverResponse.statusCode(), config)
@@ -121,6 +125,8 @@ public static class RetryConfig {
121125

122126
private Set<HttpMethod> methods = new HashSet<>(List.of(HttpMethod.GET));
123127

128+
private boolean cacheBody = false;
129+
124130
// TODO: individual statuses
125131
// TODO: backoff
126132
// TODO: support more Spring Retry policies
@@ -176,6 +182,15 @@ public RetryConfig addMethods(HttpMethod... methods) {
176182
return this;
177183
}
178184

185+
public boolean isCacheBody() {
186+
return cacheBody;
187+
}
188+
189+
public RetryConfig setCacheBody(boolean cacheBody) {
190+
this.cacheBody = cacheBody;
191+
return this;
192+
}
193+
179194
}
180195

181196
private static class RetryException extends NestedRuntimeException {

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
import java.util.List;
2828
import java.util.Locale;
2929
import java.util.Map;
30-
import java.util.concurrent.ConcurrentHashMap;
31-
import java.util.concurrent.atomic.AtomicInteger;
3230
import java.util.function.Predicate;
3331

3432
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -41,8 +39,6 @@
4139
import jakarta.servlet.ServletRequest;
4240
import jakarta.servlet.ServletResponse;
4341
import jakarta.servlet.http.HttpServletRequest;
44-
import org.apache.commons.logging.Log;
45-
import org.apache.commons.logging.LogFactory;
4642
import org.assertj.core.api.Assertions;
4743
import org.junit.jupiter.api.Test;
4844
import org.junit.jupiter.api.extension.ExtendWith;
@@ -83,10 +79,8 @@
8379
import org.springframework.util.LinkedMultiValueMap;
8480
import org.springframework.util.MultiValueMap;
8581
import org.springframework.util.StreamUtils;
86-
import org.springframework.web.bind.annotation.GetMapping;
8782
import org.springframework.web.bind.annotation.PostMapping;
8883
import org.springframework.web.bind.annotation.RequestBody;
89-
import org.springframework.web.bind.annotation.RequestParam;
9084
import org.springframework.web.bind.annotation.RestController;
9185
import org.springframework.web.servlet.function.HandlerFunction;
9286
import org.springframework.web.servlet.function.RouterFunction;
@@ -121,7 +115,6 @@
121115
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader;
122116
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeadersIfNotPresent;
123117
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestParameter;
124-
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
125118
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo;
126119
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.removeRequestHeader;
127120
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.rewritePath;
@@ -130,7 +123,6 @@
130123
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setRequestHostHeader;
131124
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
132125
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
133-
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
134126
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
135127
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.forward;
136128
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -398,20 +390,6 @@ public void circuitBreakerInvalidFallbackThrowsException() {
398390
// @formatter:on
399391
}
400392

401-
@Test
402-
public void retryWorks() {
403-
restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
404-
// test for: java.lang.IllegalArgumentException: You have already selected another
405-
// retry policy
406-
restClient.get()
407-
.uri("/retry?key=get2")
408-
.exchange()
409-
.expectStatus()
410-
.isOk()
411-
.expectBody(String.class)
412-
.isEqualTo("3");
413-
}
414-
415393
@Test
416394
public void rateLimitWorks() {
417395
restClient.get().uri("/anything/ratelimit").exchange().expectStatus().isOk();
@@ -1014,11 +992,6 @@ TestHandler testHandler() {
1014992
return new TestHandler();
1015993
}
1016994

1017-
@Bean
1018-
RetryController retryController() {
1019-
return new RetryController();
1020-
}
1021-
1022995
@Bean
1023996
EventController eventController() {
1024997
return new EventController();
@@ -1203,19 +1176,6 @@ public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerNoFall
12031176
// @formatter:on
12041177
}
12051178

1206-
@Bean
1207-
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
1208-
// @formatter:off
1209-
return route("testretry")
1210-
.route(path("/retry"), http())
1211-
.before(new LocalServerPortUriResolver())
1212-
.filter(retry(3))
1213-
//.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
1214-
.filter(prefixPath("/do"))
1215-
.build();
1216-
// @formatter:on
1217-
}
1218-
12191179
@Bean
12201180
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimit() {
12211181
// @formatter:off
@@ -1699,37 +1659,6 @@ public ResponseEntity<Event> messageChannelEvents(@RequestBody Event e) {
16991659

17001660
}
17011661

1702-
@RestController
1703-
protected static class RetryController {
1704-
1705-
Log log = LogFactory.getLog(getClass());
1706-
1707-
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
1708-
1709-
@GetMapping("/do/retry")
1710-
public ResponseEntity<String> retry(@RequestParam("key") String key,
1711-
@RequestParam(name = "count", defaultValue = "3") int count,
1712-
@RequestParam(name = "failStatus", required = false) Integer failStatus) {
1713-
AtomicInteger num = getCount(key);
1714-
int i = num.incrementAndGet();
1715-
log.warn("Retry count: " + i);
1716-
String body = String.valueOf(i);
1717-
if (i < count) {
1718-
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
1719-
if (failStatus != null) {
1720-
httpStatus = HttpStatus.resolve(failStatus);
1721-
}
1722-
return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken");
1723-
}
1724-
return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body);
1725-
}
1726-
1727-
AtomicInteger getCount(String key) {
1728-
return map.computeIfAbsent(key, s -> new AtomicInteger());
1729-
}
1730-
1731-
}
1732-
17331662
protected static class TestHandler implements HandlerFunction<ServerResponse> {
17341663

17351664
@Override

0 commit comments

Comments
 (0)