diff --git a/spring-cloud/spring-cloud-gateway/pom.xml b/spring-cloud/spring-cloud-gateway/pom.xml
index 0f62c031cf..c692eed7ec 100644
--- a/spring-cloud/spring-cloud-gateway/pom.xml
+++ b/spring-cloud/spring-cloud-gateway/pom.xml
@@ -1,7 +1,7 @@
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
spring-cloud-gateway
spring-cloud-gateway
@@ -49,6 +49,26 @@
spring-cloud-starter-gateway
+
+
+ org.springframework.cloud
+ spring-cloud-starter-circuitbreaker-reactor-resilience4j
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis-reactive
+
+
+
+
+ it.ozimov
+ embedded-redis
+ ${redis.version}
+ test
+
+
org.hibernate
hibernate-validator-cdi
@@ -69,8 +89,8 @@
test
- org.springframework.boot
- spring-boot-devtools
+ org.springframework.boot
+ spring-boot-devtools
@@ -84,11 +104,10 @@
- Greenwich.SR3
-
-
- 2.1.9.RELEASE
+ Hoxton.SR3
+ 2.2.6.RELEASE
6.0.2.Final
+ 0.7.2
diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java
new file mode 100644
index 0000000000..852e5cadba
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/WebFilterGatewayApplication.java
@@ -0,0 +1,15 @@
+package com.baeldung.springcloudgateway.webfilters;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class WebFilterGatewayApplication {
+
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(WebFilterGatewayApplication.class)
+ .profiles("webfilters")
+ .run(args);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java
new file mode 100644
index 0000000000..7b6188b66a
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/ModifyBodyRouteConfig.java
@@ -0,0 +1,49 @@
+package com.baeldung.springcloudgateway.webfilters.config;
+
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+
+import reactor.core.publisher.Mono;
+
+@Configuration
+public class ModifyBodyRouteConfig {
+
+ @Bean
+ public RouteLocator routes(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("modify_request_body", r -> r.path("/post")
+ .filters(f -> f.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
+ (exchange, s) -> Mono.just(new Hello(s.toUpperCase())))).uri("https://httpbin.org"))
+ .build();
+ }
+
+ @Bean
+ public RouteLocator responseRoutes(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("modify_response_body", r -> r.path("/put/**")
+ .filters(f -> f.modifyResponseBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
+ (exchange, s) -> Mono.just(new Hello("New Body")))).uri("https://httpbin.org"))
+ .build();
+ }
+
+ static class Hello {
+ String message;
+
+ public Hello() { }
+
+ public Hello(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+ }
+}
diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java
new file mode 100644
index 0000000000..f80a742fa6
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/config/RequestRateLimiterResolverConfig.java
@@ -0,0 +1,16 @@
+package com.baeldung.springcloudgateway.webfilters.config;
+
+import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import reactor.core.publisher.Mono;
+
+@Configuration
+public class RequestRateLimiterResolverConfig {
+
+ @Bean
+ KeyResolver userKeyResolver() {
+ return exchange -> Mono.just("1");
+ }
+}
diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/application-webfilters.yml b/spring-cloud/spring-cloud-gateway/src/main/resources/application-webfilters.yml
new file mode 100644
index 0000000000..3348cbbba0
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/main/resources/application-webfilters.yml
@@ -0,0 +1,102 @@
+logging:
+ level:
+ org.springframework.cloud.gateway: INFO
+ reactor.netty.http.client: INFO
+
+spring:
+ redis:
+ host: localhost
+ port: 6379
+ cloud:
+ gateway:
+ routes:
+ - id: request_header_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/get/**
+ filters:
+ - AddRequestHeader=My-Header-Good,Good
+ - AddRequestHeader=My-Header-Remove,Remove
+ - AddRequestParameter=var, good
+ - AddRequestParameter=var2, remove
+ - MapRequestHeader=My-Header-Good, My-Header-Bad
+ - MapRequestHeader=My-Header-Set, My-Header-Bad
+ - SetRequestHeader=My-Header-Set, Set
+ - RemoveRequestHeader=My-Header-Remove
+ - RemoveRequestParameter=var2
+ - PreserveHostHeader
+
+ - id: response_header_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/header/post/**
+ filters:
+ - AddResponseHeader=My-Header-Good,Good
+ - AddResponseHeader=My-Header-Set,Good
+ - AddResponseHeader=My-Header-Rewrite, password=12345678
+ - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
+ - AddResponseHeader=My-Header-Remove,Remove
+ - SetResponseHeader=My-Header-Set, Set
+ - RemoveResponseHeader=My-Header-Remove
+ - RewriteResponseHeader=My-Header-Rewrite, password=[^&]+, password=***
+ - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,
+ - StripPrefix=1
+
+ - id: path_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/new/post/**
+ filters:
+ - RewritePath=/new(?/?.*), $\{segment}
+ - SetPath=/post
+
+ - id: redirect_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/fake/post/**
+ filters:
+ - RedirectTo=302, https://httpbin.org
+
+ - id: status_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/delete/**
+ filters:
+ - SetStatus=401
+
+ - id: size_route
+ uri: https://httpbin.org
+ predicates:
+ - Path=/anything
+ filters:
+ - name: RequestSize
+ args:
+ maxSize: 5000000
+
+ - id: retry_test
+ uri: https://httpbin.org
+ predicates:
+ - Path=/status/502
+ filters:
+ - name: Retry
+ args:
+ retries: 3
+ statuses: BAD_GATEWAY
+ methods: GET,POST
+ backoff:
+ firstBackoff: 10ms
+ maxBackoff: 50ms
+ factor: 2
+ basedOnPreviousValue: false
+
+ - id: request_rate_limiter
+ uri: https://httpbin.org
+ predicates:
+ - Path=/redis/get/**
+ filters:
+ - StripPrefix=1
+ - name: RequestRateLimiter
+ args:
+ redis-rate-limiter.replenishRate: 10
+ redis-rate-limiter.burstCapacity: 5
+ key-resolver: "#{@userKeyResolver}"
\ No newline at end of file
diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java
new file mode 100644
index 0000000000..a28eb68775
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/RedisWebFilterFactoriesLiveTest.java
@@ -0,0 +1,64 @@
+package com.baeldung.springcloudgateway.webfilters;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.jupiter.api.RepeatedTest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+
+import redis.embedded.RedisServer;
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("webfilters")
+@TestConfiguration
+public class RedisWebFilterFactoriesLiveTest {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RedisWebFilterFactoriesLiveTest.class);
+
+ private RedisServer redisServer;
+
+ public RedisWebFilterFactoriesLiveTest() {
+ }
+
+ @Before
+ public void postConstruct() {
+ this.redisServer = new RedisServer(6379);
+ redisServer.start();
+ }
+
+ @LocalServerPort
+ String port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Autowired
+ TestRestTemplate template;
+
+ @RepeatedTest(25)
+ public void whenCallRedisGetThroughGateway_thenOKStatusOrIsReceived() {
+ String url = "http://localhost:" + port + "/redis/get";
+
+ ResponseEntity r = restTemplate.getForEntity(url, String.class);
+ // assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ LOGGER.info("Received: status->{}, reason->{}, remaining->{}",
+ r.getStatusCodeValue(), r.getStatusCode().getReasonPhrase(),
+ r.getHeaders().get("X-RateLimit-Remaining"));
+ }
+
+ @After
+ public void preDestroy() {
+ redisServer.stop();
+ }
+
+}
diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java
new file mode 100644
index 0000000000..67e00a42fc
--- /dev/null
+++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java
@@ -0,0 +1,136 @@
+package com.baeldung.springcloudgateway.webfilters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.assertj.core.api.Condition;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("webfilters")
+public class WebFilterFactoriesLiveTest {
+
+ @LocalServerPort
+ String port;
+
+ @Autowired
+ private WebTestClient client;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @BeforeEach
+ public void configureClient() {
+ client = WebTestClient.bindToServer()
+ .baseUrl("http://localhost:" + port)
+ .build();
+ }
+
+ @Test
+ public void whenCallGetThroughGateway_thenAllHTTPRequestHeadersParametersAreSet() throws JSONException {
+ String url = "http://localhost:" + port + "/get";
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ JSONObject json = new JSONObject(response.getBody());
+ JSONObject headers = json.getJSONObject("headers");
+ assertThat(headers.getString("My-Header-Good")).isEqualTo("Good");
+ assertThat(headers.getString("My-Header-Bad")).isEqualTo("Good");
+ assertThat(headers.getString("My-Header-Set")).isEqualTo("Set");
+ assertTrue(headers.isNull("My-Header-Remove"));
+ JSONObject vars = json.getJSONObject("args");
+ assertThat(vars.getString("var")).isEqualTo("good");
+ }
+
+ @Test
+ public void whenCallHeaderPostThroughGateway_thenAllHTTPResponseHeadersAreSet() {
+ ResponseSpec response = client.post()
+ .uri("/header/post")
+ .exchange();
+
+ response.expectStatus()
+ .isOk()
+ .expectHeader()
+ .valueEquals("My-Header-Rewrite", "password=***")
+ .expectHeader()
+ .valueEquals("My-Header-Set", "Set")
+ .expectHeader()
+ .valueEquals("My-Header-Good", "Good")
+ .expectHeader()
+ .doesNotExist("My-Header-Remove");
+ }
+
+ @Test
+ public void whenCallPostThroughGateway_thenBodyIsRetrieved() throws JSONException {
+ String url = "http://localhost:" + port + "/post";
+
+ HttpEntity entity = new HttpEntity<>("content", new HttpHeaders());
+
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ JSONObject json = new JSONObject(response.getBody());
+ JSONObject data = json.getJSONObject("json");
+ assertThat(data.getString("message")).isEqualTo("CONTENT");
+ }
+
+ @Test
+ public void whenCallPutThroughGateway_thenBodyIsRetrieved() throws JSONException {
+ String url = "http://localhost:" + port + "/put";
+
+ HttpEntity entity = new HttpEntity<>("CONTENT", new HttpHeaders());
+
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+ JSONObject json = new JSONObject(response.getBody());
+ assertThat(json.getString("message")).isEqualTo("New Body");
+ }
+
+ @Test
+ public void whenCallDeleteThroughGateway_thenIsUnauthorizedCodeIsSet() {
+ ResponseSpec response = client.delete()
+ .uri("/delete")
+ .exchange();
+
+ response.expectStatus()
+ .isUnauthorized();
+ }
+
+ @Test
+ public void whenCallFakePostThroughGateway_thenIsUnauthorizedCodeIsSet() {
+ ResponseSpec response = client.post()
+ .uri("/fake/post")
+ .exchange();
+
+ response.expectStatus()
+ .is3xxRedirection();
+ }
+
+ @Test
+ public void whenCallStatus504ThroughGateway_thenCircuitBreakerIsExecuted() throws JSONException {
+ String url = "http://localhost:" + port + "/status/504";
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+
+ JSONObject json = new JSONObject(response.getBody());
+ assertThat(json.getString("url")).contains("anything");
+ }
+}
diff --git a/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml b/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml
index 6fcdb6317f..6980d119b1 100644
--- a/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml
+++ b/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml
@@ -3,7 +3,15 @@
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
\ No newline at end of file