BAEL-7125: Global Exception Handling with Spring Cloud Gateway (#15566)
* BAEL-7125: Global Exception Handling with Spring Cloud Gateway * Fix name * Fix condition
This commit is contained in:
		
							parent
							
								
									a94bf971f5
								
							
						
					
					
						commit
						99832d0d4d
					
				| @ -36,6 +36,8 @@ | ||||
| 
 | ||||
|     <properties> | ||||
|         <spring.version>6.1.2</spring.version> | ||||
|         <spring-cloud.version>2023.0.0</spring-cloud.version> | ||||
|         <spring-boot.version>3.2.1</spring-boot.version> | ||||
|     </properties> | ||||
| 
 | ||||
| </project> | ||||
|  | ||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ | ||||
|     <artifactId>parent-modules</artifactId> | ||||
|     <version>1.0.0-SNAPSHOT</version> | ||||
|     <name>parent-modules</name> | ||||
| 
 | ||||
|     <packaging>pom</packaging> | ||||
| 
 | ||||
|     <dependencies> | ||||
| @ -410,6 +411,7 @@ | ||||
|                 <module>parent-spring-6</module> | ||||
|                  | ||||
|                 <module>spring-4</module> | ||||
|                 <module>spring-6</module> | ||||
| 
 | ||||
|                 <module>spring-cloud-modules</module> | ||||
|                 <!-- <module>spring-cloud-cli</module> --> <!-- Not a maven project --> | ||||
|  | ||||
							
								
								
									
										100
									
								
								spring-6/api-gateway/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								spring-6/api-gateway/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
|          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
| 
 | ||||
|     <parent> | ||||
|         <groupId>com.baeldung</groupId> | ||||
|         <artifactId>parent-spring-6</artifactId> | ||||
|         <version>0.0.1-SNAPSHOT</version> | ||||
|         <relativePath>../../parent-spring-6</relativePath> | ||||
|     </parent> | ||||
| 
 | ||||
|     <artifactId>api-gateway</artifactId> | ||||
|     <name>api-gateway</name> | ||||
|     <packaging>jar</packaging> | ||||
|     <version>1.0.0-SNAPSHOT</version> | ||||
| 
 | ||||
|     <properties> | ||||
|         <maven.compiler.source>17</maven.compiler.source> | ||||
|         <maven.compiler.target>17</maven.compiler.target> | ||||
|         <source.encoding>UTF-8</source.encoding> | ||||
|         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||
|         <logback.version>1.4.14</logback.version> | ||||
|         <slf4j.version>2.0.9</slf4j.version> | ||||
|     </properties> | ||||
| 
 | ||||
|     <dependencies> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.cloud</groupId> | ||||
|             <artifactId>spring-cloud-starter</artifactId> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.cloud</groupId> | ||||
|             <artifactId>spring-cloud-starter-gateway</artifactId> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.cloud</groupId> | ||||
|             <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> | ||||
|             <scope>test</scope> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.boot</groupId> | ||||
|             <artifactId>spring-boot-starter-test</artifactId> | ||||
|             <scope>test</scope> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>io.projectreactor</groupId> | ||||
|             <artifactId>reactor-test</artifactId> | ||||
|             <scope>test</scope> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>ch.qos.logback</groupId> | ||||
|             <artifactId>logback-core</artifactId> | ||||
|             <version>${logback.version}</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>org.slf4j</groupId> | ||||
|             <artifactId>slf4j-api</artifactId> | ||||
|             <version>${slf4j.version}</version> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|     <dependencyManagement> | ||||
|         <dependencies> | ||||
|             <dependency> | ||||
|                 <!-- Import dependency management from Spring Boot --> | ||||
|                 <groupId>org.springframework.boot</groupId> | ||||
|                 <artifactId>spring-boot-dependencies</artifactId> | ||||
|                 <version>${spring-boot.version}</version> | ||||
|                 <type>pom</type> | ||||
|                 <scope>import</scope> | ||||
|             </dependency> | ||||
| 
 | ||||
|             <dependency> | ||||
|                 <groupId>org.springframework.cloud</groupId> | ||||
|                 <artifactId>spring-cloud-dependencies</artifactId> | ||||
|                 <version>${spring-cloud.version}</version> | ||||
|                 <type>pom</type> | ||||
|                 <scope>import</scope> | ||||
|             </dependency> | ||||
|         </dependencies> | ||||
|     </dependencyManagement> | ||||
| 
 | ||||
|     <build> | ||||
|         <plugins> | ||||
|             <plugin> | ||||
|                 <groupId>org.springframework.boot</groupId> | ||||
|                 <artifactId>spring-boot-maven-plugin</artifactId> | ||||
|                 <version>${spring-boot.version}</version> | ||||
|             </plugin> | ||||
|         </plugins> | ||||
|     </build> | ||||
| 
 | ||||
| </project> | ||||
| @ -0,0 +1,70 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| import org.springframework.boot.autoconfigure.web.WebProperties; | ||||
| import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; | ||||
| import org.springframework.boot.web.error.ErrorAttributeOptions; | ||||
| import org.springframework.boot.web.reactive.error.ErrorAttributes; | ||||
| import org.springframework.context.ApplicationContext; | ||||
| import org.springframework.core.annotation.Order; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.http.HttpStatusCode; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.http.codec.ServerCodecConfigurer; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.reactive.function.BodyInserters; | ||||
| import org.springframework.web.reactive.function.server.RequestPredicates; | ||||
| import org.springframework.web.reactive.function.server.RouterFunction; | ||||
| import org.springframework.web.reactive.function.server.RouterFunctions; | ||||
| import org.springframework.web.reactive.function.server.ServerRequest; | ||||
| import org.springframework.web.reactive.function.server.ServerResponse; | ||||
| import org.springframework.web.server.ResponseStatusException; | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| @Component | ||||
| @Order(Integer.MIN_VALUE) | ||||
| class CustomGlobalExceptionHandler extends AbstractErrorWebExceptionHandler { | ||||
| 
 | ||||
|     public CustomGlobalExceptionHandler(final ErrorAttributes errorAttributes, | ||||
|                                         final WebProperties.Resources resources, | ||||
|                                         final ApplicationContext applicationContext, | ||||
|                                         final ServerCodecConfigurer configurer) { | ||||
|         super(errorAttributes, resources, applicationContext); | ||||
|         setMessageReaders(configurer.getReaders()); | ||||
|         setMessageWriters(configurer.getWriters()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { | ||||
|         return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); | ||||
|     } | ||||
| 
 | ||||
|     private Mono<ServerResponse> renderErrorResponse(ServerRequest request) { | ||||
| 
 | ||||
|         ErrorAttributeOptions options = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE); | ||||
|         Map<String, Object> errorPropertiesMap = getErrorAttributes(request, options); | ||||
|         Throwable throwable = getError(request); | ||||
|         HttpStatusCode httpStatus = determineHttpStatus(throwable); | ||||
| 
 | ||||
|         errorPropertiesMap.put("status", httpStatus.value()); | ||||
|         errorPropertiesMap.remove("error"); | ||||
| 
 | ||||
|         return ServerResponse.status(httpStatus) | ||||
|                 .contentType(MediaType.APPLICATION_JSON_UTF8) | ||||
|                 .body(BodyInserters.fromObject(errorPropertiesMap)); | ||||
|     } | ||||
| 
 | ||||
|     private HttpStatusCode determineHttpStatus(Throwable throwable) { | ||||
| 
 | ||||
|         if (throwable instanceof ResponseStatusException) { | ||||
|             return ((ResponseStatusException) throwable).getStatusCode(); | ||||
|         } else if (throwable instanceof CustomRequestAuthException) { | ||||
|             return HttpStatus.UNAUTHORIZED; | ||||
|         } else if (throwable instanceof RateLimitRequestException) { | ||||
|             return HttpStatus.TOO_MANY_REQUESTS; | ||||
|         } else { | ||||
|             return HttpStatus.INTERNAL_SERVER_ERROR; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| public class CustomRequestAuthException extends RuntimeException { | ||||
|     public CustomRequestAuthException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,75 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.boot.SpringApplication; | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||
| import org.springframework.boot.autoconfigure.web.WebProperties; | ||||
| import org.springframework.cloud.gateway.filter.GatewayFilterChain; | ||||
| import org.springframework.cloud.gateway.route.RouteLocator; | ||||
| import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.http.server.reactive.ServerHttpRequest; | ||||
| import org.springframework.web.server.ServerWebExchange; | ||||
| import org.springframework.web.util.UriComponentsBuilder; | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; | ||||
| import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; | ||||
| 
 | ||||
| @SpringBootApplication | ||||
| public class Main { | ||||
| 
 | ||||
|     @Bean | ||||
|     public WebProperties.Resources resources() { | ||||
|         return new WebProperties.Resources(); | ||||
|     } | ||||
| 
 | ||||
|     @Bean | ||||
|     public RouteLocator customRouteLocator(RouteLocatorBuilder builder, | ||||
|                                            @Value("${httpbin}") String httpbin, | ||||
|                                            MyCustomFilter myCustomFilter) { | ||||
|         return builder.routes() | ||||
|                 .route("error_404", r -> r.path("/test/error_404") | ||||
|                         .filters(f -> f.filter(myCustomFilter).filter((exchange, chain) -> switchRequestUri(exchange, chain, "error_404", "404"))) | ||||
|                         .uri(httpbin + "/status/404")) | ||||
|                 .route("error_500", r -> r.path("/test/error_500") | ||||
|                         .filters(f -> f.filter(myCustomFilter).filter((exchange, chain) -> switchRequestUri(exchange, chain, "error_500", "500"))) | ||||
|                         .uri(httpbin+"/status/500")) | ||||
|                 .route("error_400", r -> r.path("/test/error_400") | ||||
|                         .filters(f -> f.filter(myCustomFilter).filter((exchange, chain) -> switchRequestUri(exchange, chain, "error_400", "400"))) | ||||
|                         .uri(httpbin+"/status/400")) | ||||
|                 .route("error_409", r -> r.path("/test/error_409") | ||||
|                         .filters(f -> f.filter(myCustomFilter).filter((exchange, chain) -> switchRequestUri(exchange, chain, "error_409", "409"))) | ||||
|                         .uri(httpbin+"/status/409")) | ||||
|                 .route("custom_rate_limit", r -> r.path("/test/custom_rate_limit").filters(f -> f.filter(myCustomFilter)).uri(httpbin+"/uuid")) | ||||
|                 .route("custom_auth", r -> r.path("/test/custom_auth").filters(f -> f.filter(myCustomFilter)).uri(httpbin+"/api/custom_auth")) | ||||
|                 .route("anything", r -> r.path("/test/anything") | ||||
|                         .filters(f -> f.changeRequestUri((exchange) -> Optional.of(UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) | ||||
|                                         .host("httpbin.org") | ||||
|                                         .port(80) | ||||
|                                         .replacePath("/anything").build().toUri()))) | ||||
|                         .uri("http://httpbin.org")) | ||||
|                 .build(); | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private Mono<Void> switchRequestUri(ServerWebExchange exchange, | ||||
|                                         GatewayFilterChain chain, | ||||
|                                         String externalUri, | ||||
|                                         String internalUri) { | ||||
|         ServerHttpRequest req = exchange.getRequest(); | ||||
|         addOriginalRequestUrl(exchange, req.getURI()); | ||||
|         String path = req.getURI().getRawPath(); | ||||
|         String newPath = path.replaceAll("/test/" + externalUri, "/status/" + internalUri); | ||||
|         ServerHttpRequest request = req.mutate().path(newPath).build(); | ||||
|         exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI()); | ||||
|         return chain.filter(exchange.mutate().request(request).build()); | ||||
|     } | ||||
| 
 | ||||
|     public static void main(String[] args) { | ||||
|         SpringApplication.run(Main.class, args); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,29 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| import org.springframework.cloud.gateway.filter.GatewayFilter; | ||||
| import org.springframework.cloud.gateway.filter.GatewayFilterChain; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.server.ServerWebExchange; | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| @Component | ||||
| public class MyCustomFilter implements GatewayFilter { | ||||
| 
 | ||||
|     @Override | ||||
|     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | ||||
| 
 | ||||
|         if (isAuthRoute(exchange) && !isAuthorization(exchange)) { | ||||
|             throw new CustomRequestAuthException("Not authorized"); | ||||
|         } | ||||
| 
 | ||||
|         return chain.filter(exchange); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean isAuthorization(ServerWebExchange exchange) { | ||||
|         return exchange.getRequest().getHeaders().containsKey("Authorization"); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean isAuthRoute(ServerWebExchange exchange) { | ||||
|         return exchange.getRequest().getURI().getPath().equals("/test/custom_auth"); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,27 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| import org.springframework.cloud.gateway.filter.GatewayFilterChain; | ||||
| import org.springframework.cloud.gateway.filter.GlobalFilter; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.server.ServerWebExchange; | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| @Component | ||||
| class MyGlobalFilter implements GlobalFilter { | ||||
| 
 | ||||
|     @Override | ||||
|     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | ||||
| 
 | ||||
|         if (hasReachedRateLimit(exchange)) { | ||||
|             throw new RateLimitRequestException("Too many requests"); | ||||
|         } | ||||
| 
 | ||||
|         return chain.filter(exchange); | ||||
|     } | ||||
| 
 | ||||
|     private boolean hasReachedRateLimit(ServerWebExchange exchange) { | ||||
|         // Simulates the rate limit being reached | ||||
|         return exchange.getRequest().getURI().getPath().contains("/test/custom_rate_limit") && (!exchange.getRequest().getHeaders().containsKey("X-RateLimit-Remaining") || | ||||
|                 Integer.parseInt(exchange.getRequest().getHeaders().getFirst("X-RateLimit-Remaining")) <= 0); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| public class RateLimitRequestException extends RuntimeException { | ||||
|     public RateLimitRequestException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| spring.application.name: api-gateway | ||||
| spring.main.web-application-type=reactive | ||||
| eureka.instance.hostname=localhost | ||||
| spring.cloud.discovery.enabled=true | ||||
| #eureka.instance.prefer-ip-address=true | ||||
| httpbin=http://httpbin.org | ||||
| 
 | ||||
| #### DEBUG #### | ||||
| #logging.level.org.springframework.cloud.gateway=DEBUG | ||||
| #spring.cloud.gateway.httpclient.wiretap=true | ||||
| #spring.cloud.gateway.httpserver.wiretap=true | ||||
| #logging.level.reactor.netty=DEBUG | ||||
							
								
								
									
										13
									
								
								spring-6/api-gateway/src/main/resources/logback.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								spring-6/api-gateway/src/main/resources/logback.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| <configuration> | ||||
| 
 | ||||
|     <appender name="out" class="ch.qos.logback.core.ConsoleAppender"> | ||||
|         <encoder> | ||||
|             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %kvp%n</pattern> | ||||
|         </encoder> | ||||
|     </appender> | ||||
| 
 | ||||
|     <root level="INFO"> | ||||
|         <appender-ref ref="out"/> | ||||
|     </root> | ||||
| 
 | ||||
| </configuration> | ||||
| @ -0,0 +1,174 @@ | ||||
| package com.baeldung.errorhandling; | ||||
| 
 | ||||
| import com.github.tomakehurst.wiremock.http.Body; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.boot.test.context.SpringBootTest; | ||||
| import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.test.web.reactive.server.WebTestClient; | ||||
| import org.springframework.web.reactive.function.BodyInserters; | ||||
| 
 | ||||
| import java.nio.charset.StandardCharsets; | ||||
| 
 | ||||
| import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; | ||||
| import static com.github.tomakehurst.wiremock.client.WireMock.post; | ||||
| import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; | ||||
| import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; | ||||
| 
 | ||||
| 
 | ||||
| @SpringBootTest( | ||||
|     webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, | ||||
|     properties = {"httpbin=http://localhost:${wiremock.server.port}/api"} | ||||
| ) | ||||
| @AutoConfigureWireMock(port = 0) | ||||
| class RouteUnitTest { | ||||
| 
 | ||||
|     @Value("${httpbin}") | ||||
|     private String httpbin; | ||||
| 
 | ||||
|     @Value(value="${local.server.port}") | ||||
|     private int port; | ||||
| 
 | ||||
|     private WebTestClient webTestClient; | ||||
| 
 | ||||
|     @BeforeEach | ||||
|     void before() { | ||||
|         webTestClient = WebTestClient | ||||
|                 .bindToServer() | ||||
|                 .baseUrl("http://localhost:" + port + "/test") | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteResponseStatusCodeIs404_thenApiGatewayShouldHandleResponse() { | ||||
|         stubFor(post(urlEqualTo("/status/404")) | ||||
|                 .willReturn( | ||||
|                     aResponse() | ||||
|                         .withStatus(404) | ||||
|                             .withResponseBody(Body.fromJsonBytes("{\"code\": 404, \"reason\": \"Not Found\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 )); | ||||
| 
 | ||||
|         webTestClient.post() | ||||
|                 .uri("/error_404") | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus().isNotFound() | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.code").isEqualTo(404) | ||||
|                 .jsonPath("$.reason").isEqualTo("Not Found"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteResponseStatusCodeIs400_thenApiGatewayShouldHandleResponse() { | ||||
|         stubFor(post(urlEqualTo("/status/400")) | ||||
|                 .willReturn( | ||||
|                         aResponse() | ||||
|                                 .withStatus(400) | ||||
|                                 .withResponseBody(Body.fromJsonBytes("{\"code\": 400, \"reason\": \"Bad Request\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 )); | ||||
| 
 | ||||
|         webTestClient.post() | ||||
|                 .uri("/error_400") | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus().isBadRequest() | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.code").isEqualTo(400) | ||||
|                 .jsonPath("$.reason").isEqualTo("Bad Request"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteResponseStatusCodeIs409_thenApiGatewayShouldHandleResponse() { | ||||
|         stubFor(post(urlEqualTo("/status/409")) | ||||
|                 .willReturn( | ||||
|                         aResponse() | ||||
|                                 .withStatus(409) | ||||
|                                 .withResponseBody(Body.fromJsonBytes("{\"code\": 409, \"reason\": \"Conflict\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 )); | ||||
| 
 | ||||
|         webTestClient.post() | ||||
|                 .uri("/error_409") | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus().isEqualTo(409) | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.code").isEqualTo(409) | ||||
|                 .jsonPath("$.reason").isEqualTo("Conflict"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteResponseStatusCodeIs500_thenApiGatewayShouldHandleResponse() { | ||||
|         stubFor(post(urlEqualTo("/status/500")) | ||||
|                 .willReturn( | ||||
|                         aResponse() | ||||
|                                 .withStatus(500) | ||||
|                                 .withResponseBody(Body.fromJsonBytes("{\"code\": 500, \"reason\": \"Error\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 )); | ||||
| 
 | ||||
|         webTestClient.post() | ||||
|                 .uri("/error_500") | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus().isEqualTo(500) | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.code").isEqualTo(500) | ||||
|                 .jsonPath("$.reason").isEqualTo("Error"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteAnythingRequest_thenApiGatewayShouldHandleResponse() { | ||||
|         webTestClient.post() | ||||
|                 .uri("/anything") | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .body(BodyInserters.fromValue("{\"code\": 500, \"reason\": \"Error\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus() | ||||
|                 .isOk() | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.data").isEqualTo("{\"code\": 500, \"reason\": \"Error\"}"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteCustomRequestAuth_thenApiGatewayShouldHandleResponse() { | ||||
|         webTestClient.post() | ||||
|                 .uri("/custom_auth") | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .body(BodyInserters.fromValue("{\"test\": \"test\", \"message\": \"test\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus() | ||||
|                 .isUnauthorized() | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.path").isNotEmpty() | ||||
|                 .jsonPath("$.message").isEqualTo("Not authorized") | ||||
|                 .jsonPath("$.status").isEqualTo(401) | ||||
|                 .jsonPath("$.requestId").isNotEmpty() | ||||
|                 .jsonPath("$.timestamp").isNotEmpty(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void whenRouteCustomRequestRateLimit_thenApiGatewayShouldHandleResponse() { | ||||
|         webTestClient.post() | ||||
|                 .uri("/custom_rate_limit") | ||||
|                 .contentType(MediaType.valueOf("application/json")) | ||||
|                 .body(BodyInserters.fromValue("{\"test\": \"test\", \"message\": \"test\"}".getBytes(StandardCharsets.UTF_8))) | ||||
|                 .accept(MediaType.valueOf("application/json")) | ||||
|                 .exchange() | ||||
|                 .expectStatus() | ||||
|                 .isEqualTo(429) | ||||
|                 .expectBody() | ||||
|                 .jsonPath("$.path").isNotEmpty() | ||||
|                 .jsonPath("$.message").isEqualTo("Too many requests") | ||||
|                 .jsonPath("$.status").isEqualTo(429) | ||||
|                 .jsonPath("$.requestId").isNotEmpty() | ||||
|                 .jsonPath("$.timestamp").isNotEmpty(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										26
									
								
								spring-6/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								spring-6/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
|          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|     <parent> | ||||
|         <groupId>com.baeldung</groupId> | ||||
|         <artifactId>parent-spring-6</artifactId> | ||||
|         <relativePath>../parent-spring-6</relativePath> | ||||
|         <version>0.0.1-SNAPSHOT</version> | ||||
|     </parent> | ||||
| 
 | ||||
|     <artifactId>spring-6</artifactId> | ||||
|     <name>spring-6</name> | ||||
|     <packaging>pom</packaging> | ||||
| 
 | ||||
|     <properties> | ||||
|         <maven.compiler.source>17</maven.compiler.source> | ||||
|         <maven.compiler.target>17</maven.compiler.target> | ||||
|         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||
|     </properties> | ||||
| 
 | ||||
|     <modules> | ||||
|         <module>api-gateway</module> | ||||
|     </modules> | ||||
| </project> | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user