Skip to content

Commit b158666

Browse files
committed
Merge branch '4.1.x' into pr/3598
2 parents c62f2b4 + 5b08d91 commit b158666

File tree

15 files changed

+282
-122
lines changed

15 files changed

+282
-122
lines changed

.github/workflows/maven.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ name: Build
55

66
on:
77
push:
8-
branches: [ main, 3.1.x ]
8+
branches: [ main, 4.1.x, 3.1.x ]
99
pull_request:
10-
branches: [ main, 3.1.x ]
10+
branches: [ main, 4.1.x, 3.1.x ]
1111

1212
jobs:
1313
build:

README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, th
224224
[[duplicate-finder-configuration]]
225225
=== Duplicate Finder configuration
226226

227-
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`.
227+
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the project's `pom.xml`.
228228

229229
.pom.xml
230230
[source,xml]

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
}

docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"@antora/atlas-extension": "1.0.0-alpha.2",
55
"@antora/collector-extension": "1.0.1",
66
"@asciidoctor/tabs": "1.0.0-beta.6",
7-
"@springio/antora-extensions": "1.14.2",
8-
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
7+
"@springio/antora-extensions": "1.14.4",
8+
"@springio/asciidoctor-extensions": "1.0.0-alpha.16"
99
}
1010
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp
3737
// no user set property, set it to false.
3838
MapPropertySource propertySource = new MapPropertySource(MULTIPART_PROPERTY_SOURCE_NAME,
3939
Map.of(MULTIPART_ENABLED_PROPERTY, Boolean.FALSE));
40-
// environment.getPropertySources().addFirst(propertySource);
40+
environment.getPropertySources().addFirst(propertySource);
4141
}
4242
}
4343

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/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ public ServerResponse handle(ServerRequest serverRequest) {
123123
private <REQUEST_OR_RESPONSE> HttpHeaders filterHeaders(List<?> filters, HttpHeaders original,
124124
REQUEST_OR_RESPONSE requestOrResponse) {
125125
HttpHeaders filtered = original;
126-
for (var filter : filters) {
126+
for (Object filter : filters) {
127127
@SuppressWarnings("unchecked")
128-
var typed = ((HttpHeadersFilter<REQUEST_OR_RESPONSE>) filter);
128+
HttpHeadersFilter<REQUEST_OR_RESPONSE> typed = ((HttpHeadersFilter<REQUEST_OR_RESPONSE>) filter);
129129
filtered = typed.apply(filtered, requestOrResponse);
130130
}
131131
return filtered;

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/predicate/GatewayRequestPredicates.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,9 @@ private static class HostPatternPredicate implements RequestPredicate, ChangePat
374374
@Override
375375
public boolean test(ServerRequest request) {
376376
String host = request.headers().firstHeader(HttpHeaders.HOST);
377+
if (host == null) {
378+
host = "";
379+
}
377380
PathContainer pathContainer = PathContainer.parsePath(host, PathContainer.Options.MESSAGE_ROUTE);
378381
PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(pathContainer);
379382
traceMatch("Pattern", this.pattern.getPatternString(), host, info != null);

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

Lines changed: 18 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222
import java.net.URI;
2323
import java.nio.charset.StandardCharsets;
2424
import java.time.Duration;
25+
import java.util.Arrays;
2526
import java.util.Collections;
2627
import java.util.List;
2728
import java.util.Locale;
2829
import java.util.Map;
29-
import java.util.concurrent.ConcurrentHashMap;
30-
import java.util.concurrent.atomic.AtomicInteger;
3130
import java.util.function.Predicate;
3231

3332
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -40,8 +39,6 @@
4039
import jakarta.servlet.ServletRequest;
4140
import jakarta.servlet.ServletResponse;
4241
import jakarta.servlet.http.HttpServletRequest;
43-
import org.apache.commons.logging.Log;
44-
import org.apache.commons.logging.LogFactory;
4542
import org.assertj.core.api.Assertions;
4643
import org.junit.jupiter.api.Test;
4744

@@ -79,10 +76,8 @@
7976
import org.springframework.util.LinkedMultiValueMap;
8077
import org.springframework.util.MultiValueMap;
8178
import org.springframework.util.StreamUtils;
82-
import org.springframework.web.bind.annotation.GetMapping;
8379
import org.springframework.web.bind.annotation.PostMapping;
8480
import org.springframework.web.bind.annotation.RequestBody;
85-
import org.springframework.web.bind.annotation.RequestParam;
8681
import org.springframework.web.bind.annotation.RestController;
8782
import org.springframework.web.servlet.function.HandlerFunction;
8883
import org.springframework.web.servlet.function.RouterFunction;
@@ -117,7 +112,6 @@
117112
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeader;
118113
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestHeadersIfNotPresent;
119114
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.addRequestParameter;
120-
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath;
121115
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.redirectTo;
122116
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.removeRequestHeader;
123117
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.rewritePath;
@@ -126,7 +120,6 @@
126120
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setRequestHostHeader;
127121
import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.stripPrefix;
128122
import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb;
129-
import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry;
130123
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
131124
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.forward;
132125
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@@ -393,20 +386,6 @@ public void circuitBreakerInvalidFallbackThrowsException() {
393386
// @formatter:on
394387
}
395388

396-
@Test
397-
public void retryWorks() {
398-
restClient.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3");
399-
// test for: java.lang.IllegalArgumentException: You have already selected another
400-
// retry policy
401-
restClient.get()
402-
.uri("/retry?key=get2")
403-
.exchange()
404-
.expectStatus()
405-
.isOk()
406-
.expectBody(String.class)
407-
.isEqualTo("3");
408-
}
409-
410389
@Test
411390
public void rateLimitWorks() {
412391
restClient.get().uri("/anything/ratelimit").exchange().expectStatus().isOk();
@@ -484,7 +463,7 @@ public void rewritePathPostWorks() {
484463
@Test
485464
public void rewritePathPostLocalWorks() {
486465
restClient.post()
487-
.uri("/baz/post")
466+
.uri("/baz/localpost")
488467
.bodyValue("hello")
489468
.header("Host", "www.rewritepathpostlocal.org")
490469
.exchange()
@@ -636,8 +615,21 @@ private MultiValueMap<String, HttpEntity<?>> createMultipartData() {
636615
private void assertMultipartData(Map responseBody) {
637616
Map<String, Object> files = (Map<String, Object>) responseBody.get("files");
638617
assertThat(files).containsKey("imgpart");
639-
String file = (String) files.get("imgpart");
640-
assertThat(file).startsWith("data:").contains(";base64,");
618+
Object imgpart = files.get("imgpart");
619+
if (imgpart instanceof List l) {
620+
String file = (String) l.get(0);
621+
assertThat(isPNG(file.getBytes()));
622+
}
623+
else {
624+
String file = (String) imgpart;
625+
assertThat(file).startsWith("data:").contains(";base64,");
626+
}
627+
}
628+
629+
private static boolean isPNG(byte[] bytes) {
630+
byte[] pngSignature = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
631+
byte[] header = Arrays.copyOf(bytes, pngSignature.length);
632+
return Arrays.equals(pngSignature, header);
641633
}
642634

643635
@Test
@@ -991,11 +983,6 @@ TestHandler testHandler() {
991983
return new TestHandler();
992984
}
993985

994-
@Bean
995-
RetryController retryController() {
996-
return new RetryController();
997-
}
998-
999986
@Bean
1000987
EventController eventController() {
1001988
return new EventController();
@@ -1180,19 +1167,6 @@ public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerNoFall
11801167
// @formatter:on
11811168
}
11821169

1183-
@Bean
1184-
public RouterFunction<ServerResponse> gatewayRouterFunctionsRetry() {
1185-
// @formatter:off
1186-
return route("testretry")
1187-
.route(path("/retry"), http())
1188-
.before(new LocalServerPortUriResolver())
1189-
.filter(retry(3))
1190-
//.filter(retry(config -> config.setRetries(3).setSeries(Set.of(HttpStatus.Series.SERVER_ERROR)).setMethods(Set.of(HttpMethod.GET, HttpMethod.POST))))
1191-
.filter(prefixPath("/do"))
1192-
.build();
1193-
// @formatter:on
1194-
}
1195-
11961170
@Bean
11971171
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimit() {
11981172
// @formatter:off
@@ -1279,8 +1253,7 @@ public RouterFunction<ServerResponse> gatewayRouterFunctionsForm() {
12791253
// @formatter:off
12801254
return route("testform")
12811255
.POST("/post", host("**.testform.org"), http())
1282-
.before(new LocalServerPortUriResolver())
1283-
.filter(prefixPath("/test"))
1256+
.filter(new HttpbinUriResolver())
12841257
.filter(addRequestHeader("X-Test", "form"))
12851258
.build();
12861259
// @formatter:on
@@ -1677,37 +1650,6 @@ public ResponseEntity<Event> messageChannelEvents(@RequestBody Event e) {
16771650

16781651
}
16791652

1680-
@RestController
1681-
protected static class RetryController {
1682-
1683-
Log log = LogFactory.getLog(getClass());
1684-
1685-
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
1686-
1687-
@GetMapping("/do/retry")
1688-
public ResponseEntity<String> retry(@RequestParam("key") String key,
1689-
@RequestParam(name = "count", defaultValue = "3") int count,
1690-
@RequestParam(name = "failStatus", required = false) Integer failStatus) {
1691-
AtomicInteger num = getCount(key);
1692-
int i = num.incrementAndGet();
1693-
log.warn("Retry count: " + i);
1694-
String body = String.valueOf(i);
1695-
if (i < count) {
1696-
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
1697-
if (failStatus != null) {
1698-
httpStatus = HttpStatus.resolve(failStatus);
1699-
}
1700-
return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken");
1701-
}
1702-
return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body);
1703-
}
1704-
1705-
AtomicInteger getCount(String key) {
1706-
return map.computeIfAbsent(key, s -> new AtomicInteger());
1707-
}
1708-
1709-
}
1710-
17111653
protected static class TestHandler implements HandlerFunction<ServerResponse> {
17121654

17131655
@Override

0 commit comments

Comments
 (0)