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> |     <properties> | ||||||
|         <spring.version>6.1.2</spring.version> |         <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> |     </properties> | ||||||
| 
 | 
 | ||||||
| </project> | </project> | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ | |||||||
|     <artifactId>parent-modules</artifactId> |     <artifactId>parent-modules</artifactId> | ||||||
|     <version>1.0.0-SNAPSHOT</version> |     <version>1.0.0-SNAPSHOT</version> | ||||||
|     <name>parent-modules</name> |     <name>parent-modules</name> | ||||||
|  | 
 | ||||||
|     <packaging>pom</packaging> |     <packaging>pom</packaging> | ||||||
| 
 | 
 | ||||||
|     <dependencies> |     <dependencies> | ||||||
| @ -410,6 +411,7 @@ | |||||||
|                 <module>parent-spring-6</module> |                 <module>parent-spring-6</module> | ||||||
|                  |                  | ||||||
|                 <module>spring-4</module> |                 <module>spring-4</module> | ||||||
|  |                 <module>spring-6</module> | ||||||
| 
 | 
 | ||||||
|                 <module>spring-cloud-modules</module> |                 <module>spring-cloud-modules</module> | ||||||
|                 <!-- <module>spring-cloud-cli</module> --> <!-- Not a maven project --> |                 <!-- <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