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

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();
}
}

Related

#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.

MediaTypeNotAcceptable with SpringBoot RestController

I have a rest controller with a GetMapping that produces media type "Plain_text". When an exception occurs in the underlying service, it will be handled by the controller advice and the controller advice returns an object that will be serialized to JSON.
In the happy path, where the service doesn't throw any exception, I'm getting a correct response. But in case of error scenarios, I'm getting an exception with error "Could not find acceptable representation". If I removed the produces tag, the controller is working fine.
Is there a way in spring boot to let an api return plain text media type and in case of errors, return a Json response?
Here is my code:
#RestController
#RequestMapping("/sample")
public class SampleController() {
#Autowired
SampleService service;
#GetMapping(produces = MediaType.TEXT_PLAIN)
public String getString(){
return service.getString();
}
}
ControllerAdvice:
#RestControllerAdvice
public class SampleControllerAdvice(){
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({SampleNotFoundException.class})
public SampleErrorResponse handleException(Exception ex) {
return new SampleErrorResponse(e.getMessage());
}
}
This looks related to SPR-16318, which has been fixed in Spring Framework 5.1 - this is the version used in Spring Boot 2.1.
You should upgrade to Spring Boot 2.1+ to get that fix in your application.

Throwing Custom Runtime exception in Spring Cloud Gateway Filters

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());
};
}

#RestControllerAdvice vs #ControllerAdvice

What are the major difference between #RestControllerAdvice and #ControllerAdvice ??
Is it we should always use #RestControllerAdvice for rest services and #ControllerAdvice MVC ?
#RestControllerAdvice is just a syntactic sugar for #ControllerAdvice + #ResponseBody, you can look here.
Is it we should always use #RestControllerAdvice for rest services and
#ControllerAdvice MVC?
Again, as mentioned above, #ControllerAdvice can be used even for REST web services as well, but you need to additionally use #ResponseBody.
In addition, we can just understand it as:
#RestControler = #Controller + #ResponseBody
#RestControllerAdvice = #ControllerAdvice + #ResponseBody.
Keeping in mind that #RestControllerAdvice is more convenient annotation for handling Exception with RestfulApi.
Example os usage:
#RestControllerAdvice
public class WebRestControllerAdvice {
#ExceptionHandler(CustomNotFoundException.class)
public ResponseMsg handleNotFoundException(CustomNotFoundException ex) {
ResponseMsg responseMsg = new ResponseMsg(ex.getMessage());
return responseMsg;
}
}
In that case any exception instanceOf CustomNotFoundException will be thrown in body of response.
Example extracted here:
https://grokonez.com/spring-framework/spring-mvc/use-restcontrolleradvice-new-features-spring-framework-4-3
Exception: A good REST API should handle the exception properly and send the proper response to the user. The user should not be rendered with any unhandled exception.
A REST API developer will have two requirements related to error handling.
Common place for Error handling
Similar Error Response body with a proper HTTP status code across APIs
#RestControllerAdvice is the combination of both #ControllerAdvice and #ResponseBody
The #ControllerAdvice annotation was first introduced in Spring 3.2.
We can use the #ControllerAdvice annotation for handling exceptions in the RESTful Services but we need to add #ResponseBody separately.
Note:
GlobalExceptionHandler was annotated with #ControllerAdvice, thus it is going to intercept exceptions from controllers accross the application.
The differences between #RestControllerAdvice and #ControllerAdvice is :
#RestControllerAdvice = #ControllerAdvice + #ResponseBody. - we can
use in REST web services.
#ControllerAdvice - We can use in both MVC and Rest web services, need to
provide the ResponseBody if we use this in Rest web services.
For Example :
Exception Class:
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception{
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message){
super(message);
}
}
usage of the above exception in Rest Web Service.
#RestControllerAdvice
public class MyRestControllerAdviceHandler {
#ExceptionHandler(ResourceNotFoundException.class)
public ResponseMsg resourceNotFoundException(ResourceNotFoundException ex) {
ResponseMsg resMsg = new ResponseMsg(ex.getMessage());
return resMsg;
}
}
usage of the above exception in MVC.
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
If you use #ControllerAdvice and return your error object from a method then it will look for a view with the name of your error object so instead of returning the expected response it will return 404 for not founding a view page with that name
#ControllerAdvice
public class CustomizedExceptionHandler {
#ExceptionHandler({ UserNotFoundException.class })
#ResponseStatus(code = HttpStatus.BAD_REQUEST)
public ExceptionResponce handleUserNotException(Exception ex, WebRequest request) throws Exception {
ExceptionResponce exceptionResponce = new ExceptionResponce(new Date(), ex.getMessage(),
request.getDescription(false));
return exceptionResponce;
}
}
As in the above code, I want to return 400 (BAD_REQUEST) but
instead of 400, it is returning 404(NOT_FOUND)
You can solve this issue by using any of the below ways
add #ResponseBody to your method or class.
Use #RestControllerAdvice.
Or you can wrap your error object in ResponseEntity.
After using either of the above ways it returns the correct response

Stacktrace of exceptions in Spring Rest responses

I have few Rest web services implemented through Spring. The problem is that if any exception is thrown the webservice returns json object with formatted error message that contains stacktrace. Can I have a single point of handling exceptions, and return my custom json objects with messages that wouldn't contain stacktrace?
I see descriptions for spring mvc but im not really using that for building my views etc.
I know it's too late, but just pointing out some solutions that may help others!
case 1: if you're using application.properties file, add following line to your properties file.
server.error.include-stacktrace=on_trace_param
case 2: if you're using application.yml file, add following line to your yml file.
server:
error:
include-stacktrace: on_trace_param
case 3: In case, none of them works, try following changes:
Try to suppress the stack trace by overriding fillInStackTrace method in your exception class as below.
public class DuplicateFoundException extends RuntimeException {
#Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
ps1: I referred this article.
Spring provides an out of the box solution to handle all your custom exceptions from a single point. What you need is #ControllerAdvice annotation in your exception controller:
#ControllerAdvice
public class GlobalDefaultExceptionHandler {
#ExceptionHandler(Exception.class)
public String exception(Exception e) {
return "error";
}
}
If you want to go deep into Springs #ExceptionHandler at individual controller level or #ControllerAdvice at global application level here is a good blog.
To handle exceptions thrown from a spring application at a single point, this is the best way to do it. #ControllerAdvice will create an aspect join-point which will intercept all the exceptions with required matching types bound to the corresponding public method.Here, public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException dataIntegrityViolationException,
WebRequest request) is handling DataIntegrityViolationException thrown out of the system at one place.
#ControllerAdvice
public class GlobalControllerExceptionHandler {
private Logger logger = Logger.getLogger(this.getClass());
private HttpHeaders header = new HttpHeaders();
#Autowired
private MessageSource messageSource;
public GlobalControllerExceptionHandler() {
header.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
/**
* #param dataIntegrityViolationException
* #param request
* #return
*/
#ExceptionHandler({ DataIntegrityViolationException.class })
public ResponseEntity<?> handleDataIntegrityViolationException(DataIntegrityViolationException dataIntegrityViolationException,
WebRequest request) {
String message = ExceptionUtils.getMessage(dataIntegrityViolationException);
logger.error("*********BEGIN**************DataIntegrityViolationException******************BEGIN*******************\n");
logger.error(message, dataIntegrityViolationException.fillInStackTrace());
logger.error("*********ENDS**************DataIntegrityViolationException*******************ENDS*****************************\n");
return ResponseEntity.status(HttpStatus.CONFLICT).headers(header).body(dataIntegrityViolationException);
}
}

Resources