Skip to content

Commit 159ce34

Browse files
committed
Adds support for retry with request body.
Adds MvcUtils.getOrCacheBody() used by RetryFilterFunctions.retry(). Moves retry tests into RetryFilterFunctionTests.java Fixes gh-3336
1 parent eef10ab commit 159ce34

File tree

5 files changed

+212
-72
lines changed

5 files changed

+212
-72
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/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

@@ -80,10 +76,8 @@
8076
import org.springframework.util.LinkedMultiValueMap;
8177
import org.springframework.util.MultiValueMap;
8278
import org.springframework.util.StreamUtils;
83-
import org.springframework.web.bind.annotation.GetMapping;
8479
import org.springframework.web.bind.annotation.PostMapping;
8580
import org.springframework.web.bind.annotation.RequestBody;
86-
import org.springframework.web.bind.annotation.RequestParam;
8781
import org.springframework.web.bind.annotation.RestController;
8882
import org.springframework.web.servlet.function.HandlerFunction;
8983
import org.springframework.web.servlet.function.RouterFunction;
@@ -118,7 +112,6 @@
118112
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader;
119113
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeadersIfNotPresent;
120114
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestParameter;
121-
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
122115
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo;
123116
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.removeRequestHeader;
124117
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.rewritePath;
@@ -127,7 +120,6 @@
127120
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setRequestHostHeader;
128121
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
129122
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
130-
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
131123
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
132124
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.forward;
133125
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -394,20 +386,6 @@ public void circuitBreakerInvalidFallbackThrowsException() {
394386
// @formatter:on
395387
}
396388

397-
@Test
398-
public void retryWorks() {
399-
restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
400-
// test for: java.lang.IllegalArgumentException: You have already selected another
401-
// retry policy
402-
restClient.get()
403-
.uri("/retry?key=get2")
404-
.exchange()
405-
.expectStatus()
406-
.isOk()
407-
.expectBody(String.class)
408-
.isEqualTo("3");
409-
}
410-
411389
@Test
412390
public void rateLimitWorks() {
413391
restClient.get().uri("/anything/ratelimit").exchange().expectStatus().isOk();
@@ -1005,11 +983,6 @@ TestHandler testHandler() {
1005983
return new TestHandler();
1006984
}
1007985

1008-
@Bean
1009-
RetryController retryController() {
1010-
return new RetryController();
1011-
}
1012-
1013986
@Bean
1014987
EventController eventController() {
1015988
return new EventController();
@@ -1194,19 +1167,6 @@ public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerNoFall
11941167
// @formatter:on
11951168
}
11961169

1197-
@Bean
1198-
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
1199-
// @formatter:off
1200-
return route("testretry")
1201-
.route(path("/retry"), http())
1202-
.before(new LocalServerPortUriResolver())
1203-
.filter(retry(3))
1204-
//.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
1205-
.filter(prefixPath("/do"))
1206-
.build();
1207-
// @formatter:on
1208-
}
1209-
12101170
@Bean
12111171
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimit() {
12121172
// @formatter:off
@@ -1690,37 +1650,6 @@ public ResponseEntity<Event> messageChannelEvents(@RequestBody Event e) {
16901650

16911651
}
16921652

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

17261655
@Override

0 commit comments

Comments
 (0)