Throwing Custom Runtime exception in Spring Cloud Gateway Filters - spring

We are using Spring Cloud Gateway with Spring Boot 2 and reactive WebFlux module.
There is an authentication filter which is added for one of the routes. Now if we throw a RuntimeException with a particular status code, it is really not picking up.
Earlier this authentication check was part of the HandlerInterceptor in Spring, but now we cannot use the web module along with WebFlux (conflict from Spring cloud gateway).
Example:
#Override
public GatewayFilter apply(Object config) {
ServerHttpRequest httpRequest = exchange.getRequest();
if(!someUtil.validRequest(httpRequest) {
throw new RuntimeException("Throw 401 Unauthorized with Custom error code and message");
}
}
Currently, the actual response always gives a 500 internal server error. From where is this coming from? Can we get hold of the errors from Filters here?

You can implement a custom error handler, and here is the Spring Boot document.
Or you can simply throw a ResponseStatusException. The default error handler will render the specific status.

Keep in mind, as of the time of writing, spring-cloud-gateway uses Spring Framework WebFlux. This means that the approach would be different. You can get hold of the exception in a filter as shown below.
Declare an exception like this:
public class UnauthorisedException extends ResponseStatusException {
public UnauthorisedException(HttpStatusCode status) {
super(status);
}
public UnauthorisedException(HttpStatusCode status, String reason) {
super(status, reason);
}
}
NB: The exception extends ResponseStatusException.
The ControllerAdvice class can be implemented as follows:
#ControllerAdvice
public class MyErrorWebExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(UnauthorisedException.class)
public Mono<ServerResponse> handleIllegalState(ServerWebExchange exchange, UnauthorisedException exc) {
exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
return ServerResponse.from(ErrorResponse.builder(exc,HttpStatus.FORBIDDEN,exc.getMessage()).build());
}
}
In your filter you can now implement the apply method as follows:
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().get("token") == null){ //test is an example
throw new UnauthorisedException(HttpStatus.FORBIDDEN, "Not Authorised from Gateway");
}
ServerHttpRequest.Builder builder = request.mutate();
return chain.filter(exchange.mutate().request(builder.build()).build());
};
}

Related

Spring Reactive - Exception Handling for Method Not Allowed Exception not triggering post Spring 3.0.0 & Java 17 upgrade

We recently upgraded our Spring Reactive APIs that were running on Java 11 and Spring 2.7.x. Exceptions in the Controller layer are handled by a Global Exception Handler which also handled the Method Not Supported exception. Post the upgrade, we are getting internal server error instead of Method not allowed exception when we try a different HTTP verb other that the one that a specific endpoint is designated to.
Our application has both of the below dependencies:
spring-boot-starter-web
spring-boot-starter-webflux
Searched for some stack overflow links and tried adding the below piece of code but didn't help either.
#Component
#Order(-2)
public class RestWebExceptionHandler implements ErrorWebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (ex instanceof HttpRequestMethodNotSupportedException) {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
// marks the response as complete and forbids writing to it
return exchange.getResponse().setComplete();
}
return Mono.error(ex);
}
#ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<PlanResponse> handleHttpRequestMethodNotSupportedException(
final HttpRequestMethodNotSupportedException exception) {
return responseBuilderRegistry.getResponseBuilderByType(HttpRequestMethodNotSupportedResponseBuilder.class)
.buildResponse(exception);
That was a common issue that was "recently" addressed on Spring MVC and Spring Webflux.
If you are interested in it, this issue was discussed here https://github.com/spring-projects/spring-framework/issues/22991
Just created a project to test this, can you please try the following
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(MethodNotAllowedException.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<Void> handle(Exception e, ServerWebExchange exchange) {
return exchange.getResponse().setComplete();
}
}

How to add Pre Filter in Spring cloud gateway

I am using spring cloud gateway to route request to my downstream application
I have the router defined something like below
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/test/**")
.uri("http://localhost:8081/test")
.id("testModule"))
.build();
}
}
Routing works fine, now I need to add a prefilter which can do some pre-condition and get routing path. but not getting how to change uri dynamically .uri("http://localhost:8081/test")
Below is the code I am trying for out in preFilter.
#Component
public class testPreFilter extends AbstractGatewayFilterFactory {
#Override
public GatewayFilter apply(Config config) {
System.out.println("inside testPreFilter.apply method");
return (exchange, chain) -> {
//get headers and do lookup for URI in mapping DB
**//If contains return modify the uri**
return chain.filter(exchange.mutate().request(request).build());
//else 401
};
}
}
so I need to forward from incoming path /user/test/** to http://localhost:8081/test1 or http://localhost:8081/test2 based on db lookup return in my custom filter
You are basically changing the path I believe , so you can do that in this fashion .
Based on the value you get from the database , set the path .

Returning value from Apache Camel route to Spring Boot controller

I am calling a camel route from a Spring Boot controller. The camel route calls a REST service which returns a string value and I am trying to return that value from the camel route to the controller. Below is the Spring Boot controller:
#RestController
#RequestMapping("/demo/client")
public class DemoClientController {
#Autowired private ProducerTemplate template;
#GetMapping("/sayHello")
public String sayHello() throws Exception {
String response = template.requestBody("direct:sayHelloGet", null, String.class);
return response;
}
}
And below is my camel route:
#Component
public class MyRoute extends RouteBuilder {
#Override
public void configure() throws Exception {
from("direct:sayHelloGet")
.log("Route reached")
.setHeader(Exchange.HTTP_METHOD, simple("GET"))
.to("http://localhost:8080/demo/sayHello")
.log("${body}");
}
}
In the route, the log is printing the return value from the REST service but that String is not returned to the controller. Can anyone please suggest how to return the value to the Spring Boot controller?
The Spring Boot version I am using is 2.2.5 and Apache Camel version is 3.0.1.
See this FAQ
https://camel.apache.org/manual/latest/faq/why-is-my-message-body-empty.html
The response from http is streaming based and therefore only readable once, and then its read via the log and "empty" as the response. So either
do not log
enable stream caching
convert the response from http to a string (not streaming and re-readable safe)

How implement Error decoder for multiple feign clients

I have multiple feign clients in a Spring Boot application. I am using a Controller Advice for handling custom exceptions for each feign client.
Here my controller advice that handles two custom exceptions (one for each client: client1 and client2):
#ControllerAdvice
public class ExceptionTranslator implements ProblemHandling {
#ExceptionHandler
public ResponseEntity<Problem> handleCustomClient1Exception(CustomException1 ex, NativeWebRequest request) {
Problem problem = Problem.builder()
.title(ex.getTitle())
.detail(ex.getMessage())
.status(ex.getStatusType())
.code(ex.getCode())
.build();
return create(ex, problem, request);
}
#ExceptionHandler
public ResponseEntity<Problem> handleCustomClient2Exception(CustomException2 ex, NativeWebRequest request) {
Problem problem = Problem.builder()
.title(ex.getTitle())
.detail(ex.getMessage())
.status(ex.getStatusType())
.code(ex.getCode())
.build();
return create(ex, problem, request);
}
}
I have implemented an error decoder for feign client1.
public class ClientErrorDecoder implements ErrorDecoder {
final ObjectMapper mapper;
public ClientErrorDecoder() {
this.mapper = new ObjectMapper();
}
#Override
public Exception decode(String methodKey, Response response) {
ExceptionDTO exceptionDTO;
try {
exceptionDTO = mapper.readValue(response.body().asInputStream(), ExceptionDTO.class);
} catch (IOException e) {
throw new RuntimeException("Failed to process response body.", e);
}
return new CustomException1(exceptionDTO.getDetail(), exceptionDTO.getCode(), exceptionDTO.getTitle(), exceptionDTO.getStatus());
}
}
I have also configured feign for using that error decoder for that specific client like this:
feign:
client:
config:
client1:
errorDecoder: feign.codec.ErrorDecoder.Default
My question is: what is the best approach for handling more than one feign client exceptions? Should I use the same error decoder and treat their responses as a generic exception? Or should I create an error decoder for each feign client?
Quick Answer
If you work with different APIs, error responses will not be formatted the same way. Hence handling them separately seems to be the best approach.
Remarks
From your example, it seems like you defined a custom ErrorDecoder that may
be not used because you also configured feign to use default error decoder for you client1 in properties file.
Even if you defined a #Configuration class somewhere with a bean for your custom ClientErrorDecoder,
Spring Cloud documentation mentions that configuration properties take precedence over #Configuration annotation
If we create both #Configuration bean and configuration properties,
configuration properties will win. It will override #Configuration
values. But if you want to change the priority to #Configuration, you
can change feign.client.default-to-properties to false.
Example
Here is a hypothetical pruned configuration to handle multiple feign clients with different error decoders :
Client1:
You tell feign to load beans defined in CustomFeignConfiguration class for client1
#FeignClient(name = "client1", configuration = {CustomFeignConfiguration.class})
public interface Client1 {...}
Client2:
Client2 will use default Feign ErrorDecoder because no configuration is specified. (Will throw a FeignException on error)
#FeignClient(name = "client2")
public interface Client2 {...}
Configuration: Be carefull here, if you add #Configuration to CustomFeignConfiguration, then ClientErrorDecoder bean will be used for every loaded feign clients (depending on your application component scanning behaviour)
public class CustomFeignConfiguration {
#Bean
public ClientErrorDecoder clientErrorDecoder(ObjectMapper objectMapper) {
return new ClientErrorDecoder(objectMapper);
}
}
This configuration could be done with properties file aswell.
Side remark
From my point of view, you don't even need controller advice. If you use Spring Web #ResponseStatus annotation, you can tell which HTTP status code should be sent back with exception body thrown by your custom ErrorDecoder.
Helpeful resources
Spring Cloud Documentation
GitHub issue related to the subject

#ControllerAdvice handler not being called from Spring Cloud custom filter

I have created the following custom filter to be used for authorization in a Spring Cloud application. Here is the apply() method from that filter, from which an exception is thrown should the authorization check fail:
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
HttpStatus status = HttpStatus.FORBIDDEN;
try {
String authRequest = exchange.getRequest().getHeaders().getFirst(
Constants.SESSION_HEADER_NAME);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>(authRequest, headers);
// make REST call to authorization module
status = restTemplate.postForEntity(authURL, entity, String.class).getStatusCode();
}
catch (Exception e) {
LOGGER.error("Something went wrong during authorization", e);
}
// throw an exception if anything went
// wrong with authorization
if (!HttpStatus.OK.equals(status)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return chain.filter(exchange);
};
}
I have defined the following #ControllerAdvice class to catch all exceptions thrown by the above Gateway Cloud filter:
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(value = ResponseStatusException.class)
public final ResponseEntity<Object> handleException(ResponseStatusException ex) {
return new ResponseEntity<>("UNAUTHORIZED", HttpStatus.FORBIDDEN);
}
}
What I currently observe is the following:
The above custom filter's ResponseStatusException is not being caught by the #ControllerAdvice mapping method.
Yet, throwing this exception from elsewhere in the Spring Boot application, e.g. the regular controller we use as an authentication endpoint, is caught by the #ControllerAdvice method.
For now, this is more of a nuisance than a blocker, because in fact throwing a ResponseStatusException from the Cloud filter with a custom error code in fact does return that error code to the caller. But, it would be nice to handle all exceptions in a single place.
Can anyone shed some light on this problem?
From ControllerAdvice javadocs:
Classes with #ControllerAdvice can be declared explicitly as Spring beans or auto-detected via classpath scanning.
You didn't show full class for your filter, but I bet it isn't Spring Bean scanned on classpath. Typically servlet filters are explicitly plugged into Spring Security configuration. Therefore ControllerAdvice processing is ignoring it.
I assume that by Filter you mean javax.servlet.Filter.
In that case, #ControllerAdvice cannot work. It is used to handle exceptions from Controllers. But you throw Exception before it can even propagate to Controller (by not calling the chain.filter(exchange) method.
Try throwing exceptions in controller, not in filter.
Edit: If you don't want to handle exceptions on #Controller, you must implement terminal handler in javax.servlet.Filter directly. That means change the incoming request' response directly like this:
HttpServletResponse httpResponse = (HttpServletResponse) exchange.getResponse();
// either format the response by yourself
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setHeader(...)
...
// or let populate error directly
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
... which is something what #ControllerAdvice does internally.

Resources