How to map exceptions in spring webflux controllers? - java

We use controllers in our spring webflux project, e.g.
#RestController
public class TestController {
#GetMapping("fail")
public Mono<String> getFailure() {
return Mono.error(new RuntimeException("error"));
}
}
Now, when one opens the http://localhost:8080/fail URL in a browser, they would see the "Whitelabel Error Page" coming from spring. Question: how to map RuntimeException to a custom HTTP response?
These options have no effect on the response:
#ExceptionHandler in the controller
#ExceptionHandler(Exception.class)
public Mono<ServerResponse> onException(Exception e) {
return ServerResponse
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("error response", String.class);
}
#RestControllerAdvice annotated class
#RestControllerAdvice
public class ControllerAdvice
{
#ExceptionHandler(Throwable.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<String> exceptions(Throwable e)
{
return Mono.just("error response");
}
}
ResponseStatusException instance
#GetMapping("fail")
public Mono<String> getFailure() {
return Mono.error(new RuntimeException("error"))
.onErrorResume(e -> Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "error happened")))
.map(obj -> (String) obj); // casting
}
How to map exceptions to custom HTTP responses in spring webflux when using controllers? Why don't the above approaches work?

Its because you are returning Producers. An exception is a terminal event.
If you look in the examples provided in the documentation you can see that they are not returning Mono<ServerResponse> but a plain ResponseEntity<T>.
Exception handler in a Controller.
#RestController
public class TestController {
#GetMapping("fail")
public Mono<String> getFailure() {
// return Mono.just("hello world");
return Mono.error(new NullPointerException("error"));
}
// Here i build a response entity
#ExceptionHandler(NullPointerException.class)
public ResponseEntity<String> onException(NullPointerException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
Or here in the controller advice i just return a plain String.
#RestControllerAdvice
public class ControllerAdvice {
// I can return a response entity or for instance just a plain string
#ExceptionHandler(Throwable.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String exceptions(Throwable e) {
return "Foobar";
}
}

Related

Spring Exception Handler not returning JSON response

I have created a controller advice class to return JSON error responses.
It does not respond with JSON when I get a HttpMessageNotReadableException, however I still get a "Resolved [org.springframework.http.converter.HttpMessageNotReadableException:..." log in my terminal.
My exception handler class:
#ControllerAdvice
public class PosterExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {
RuntimeException.class,
})
protected ResponseEntity<ExceptionResponse> internalServerErrorHandler(RuntimeException e) {
return this.defaultResponseHandler(e, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(value = {
UserNotFoundException.class,
UserExistsException.class,
InvalidTokenException.class
})
public ResponseEntity<ExceptionResponse> badRequestResponseHandler(RuntimeException e) {
return this.defaultResponseHandler(e, HttpStatus.BAD_REQUEST);
}
public ResponseEntity<ExceptionResponse> defaultResponseHandler(RuntimeException e, HttpStatus status) {
var resp = new ExceptionResponse(
status.value(),
e.getMessage(),
Instant.now()
);
return new ResponseEntity<>(resp, status);
}
}
The exception I receive is org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity<java.lang.Object> com.sulayman.poster.controller.PostController.post(com.sulayman.poster.dto.PostRequestDto
I expected the application to return a JSON response with the error
Because your PosterExceptionHandler extends by ResponseEntityExceptionHandler, which intercept HttpMessageNotReadableException
else if (ex instanceof HttpMessageNotReadableException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
}
If you want to handle the HttpMessageNotReadableException themselves, you need delete extend ResponseEntityExceptionHandler.

How to forward a request to a different rest controller with Spring Reactive and WebFlux

I'm trying to forward a request to a different internal endpoint from a WebFlux RestContoller.
I don't want to send a redirect to the client, but let a different controller handling the current request. See the code below:
#RestController
public class MyController {
#PostMapping("/start")
public Mono<MyReponse> start() {
return myService.retrieveSomething().flatMap(something -> {
if(something.isBlank()){
retrun createMyResponse(); // returns Mono<MyResponse>
} else {
// forward to "/continue"
}
});
}
}
#RestController
public class MyOtherController {
#PostMapping("/continue")
public Mono<MyReponse> continue() {
// do something and return Mono<MyResponse>
}
}
With Spring Servlet I'd do something like:
protected void start(HttpServletRequest req, HttpServletResponse resp) {
RequestDispatcher dispatcher = getServletContext()
.getRequestDispatcher("/continue");
dispatcher.forward(req, resp);
}
Is it possible to do the same in Spring Reactive with WebFlux?
Had a look at the official documentation but couldn't find an example. I tried to use objects like DispatcherHandler, ServerWebExchange, ServerHttpRequest, ServerHttpResponse but couldn't find a solution.
You could use Spring WebFlux Functional Endpoints that "a lightweight functional programming model in which functions are used to route and handle requests".
#Bean
RouterFunction<ServerResponse> startRoute() {
return route(POST("/start"), req -> startHandler(req));
}
#Bean
RouterFunction<ServerResponse> continueRoute() {
return route(POST("/continue"), req -> continueHandler(req));
}
Mono<ServerResponse> startHandler(ServerRequest serverRequest) {
return myService.retrieveSomething()
.flatMap(something -> {
if (something.isBlank()){
...
} else {
return continueHandler(serverRequest);
}
});
}
Mono<ServerResponse> continueHandler(ServerRequest serverRequest) {
...
}

ControllerExceptionHandler not working on a spring boot app

I have a ControllerExceptionHandler with ControllerAdvise and when in application is thrown generalException or customException then i except to catch it in this ControllerExceptionHandler . But it doesn't happen. It looks very simple, read many sites , but it's not triggering.
Don't know where is the problem.
It's ControllerExceptionHandler.class
#ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ControllerExceptionHandler.class);
#ExceptionHandler(Exception.class)
public final ResponseEntity<?> handleGeneralErrors(Exception e, UserPrincipal userPrincipal) {
if (e instanceof ClientAbortException) {
LOGGER.error("REST client abort: {}", e.getMessage());
} else {
LOGGER.error("REST controller error.", e);
}
//somelogic
return new ResponseEntity<>(responseObject, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(MyCustomException.class)
public final ResponseEntity<?> handleCustomErrors(MyCustomException e, UserPrincipal userPrincipal) {
LOGGER.error("MyCustomException error.", e);
return new ResponseEntity<>(
responseObject,
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Controller.class
#RestController
#RequestMapping(path = "/rest/test", produces = MediaType.APPLICATION_JSON_VALUE)
public class Controller {
private static final Logger LOGGER = LoggerFactory.getLogger(Controller .class);
#Autowired
private SomeLogicClass someLogicClass;
#GetMapping("/check")
#ResponseBody
public ResponseEntity<List<City>> list(UserPrincipal userPrincipal) throws Exception {
//Some logic
return ResponseEntity.ok(someLogicClass.handleRequest());
}
SomeLogicClass.class
#Service
public class SomeLogicClass{
public void handleRequest() throws Exception {
//some logic
} catch (Exception ex) {
throw new MyCustomException();
}
}
}
So, when the request is reaching SomeLogicClass, then some Exception(for example NPE) is thrown and then i want to throw in catch myCustomException. I expect that it will go to ControllerAdvise, but nothing and i see error 500.
Something is missing here or what? Controller and ControllerAdvise are located in some package.
I also tried to add the package
#ControllerAdvice("my.package.controller")
Found what caused that it skipped ControllerAdvise. The problem was UserPrinciple object, that is actually my custom class. Any custom class that i put additionally in input argument was a problem. I don't know the reason for this , but implemented without UserPrinciple.class.

How to return a error message in a method that return a ResponseEntity in Spring MVC using #ControllerAdvice

I'm working on a web app using Spring MVC and AngularJS, I'm creating a Rest API that returns ResponseEntities that contains JSON strings.
I want to be able when an Exception happens to return a string that contains the error cause to my view and then show this error with a modal in AngularJS, I created a Class with the #ControllerAdvice annotation and in this class I defined a method with my custom exception like this
#ControllerAdvice
public class GlobalExceptionHandlerController {
#ExceptionHandler(PersonalException.class)
public String handleCustomExceptionRazon(PersonalException ex) {
String errorMessage = "custom error";
return errorMessage;
}
}
I have the following interface
public interface ClientDAO {
public void insertCLiente(Client client) throws PersonalException
}
And in my implementation
#Override
public void insertCLiente(Client client) throws PersonalException{
//method implementation
if (searchCLiente(client.name())) {
throw new PersonalException("client aleady exists");
} else {
//method implementation
}
}
My searchClient Method
public boolean searchClient(String name) {
try {
//method implementation
} catch (DataAccessException dataAccessException) {
System.out.println("");
dataAccessException.printStackTrace();
} catch (Exception e) {
System.out.println("");
e.printStackTrace();
}
//method implementation
}
My Client Controller
#Autowired
ClientDAO clientDAO;
#RequestMapping(value = "/client/", method = RequestMethod.POST)
public ResponseEntity<Void> createClient(#RequestBody final String DTOClientData, UriComponentsBuilder ucBuilder) {
//here I parse the JSON data and create my Client object
//here I dont know how can I return the error message
clientDAO.insertClient(client);
}
My custom Exception
public class PersonalException extends Exception {
public PersonalException (String msg) {
super(msg);
}
}
I don't know un my clientController method createClient how can I return an execption of the type PersonalException that I created
//here I dont know how can I return the error message
Just throw the exception from Controller.
#RequestMapping(value = "/client/", method = RequestMethod.POST)
public ResponseEntity<Void> createClient(#RequestBody final String DTOClientData, UriComponentsBuilder ucBuilder) throws PersonalException {
You can return error message in GlobalExceptionHandlerController like this...
/**
* REST exception handlers defined at a global level for the application
**/
#ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = { PersonalException.class })
protected ResponseEntity<RestResponse> handleUnknownException(PersonalException ex, WebRequest request) {
LOGGER.error(ex.getMessage(), ex);
return new ResponseEntity<RestResponse>(new RestResponse(Boolean.FALSE, ImmutableList.of("Exception message"), null), HttpStatus.INTERNAL_SERVER_ERROR);
}
Now, you might have noticed that we are not handling the Exception even in the Controller. Instead, we are Throwing it in the declaration hoping that somewhere we have handled this exceptional case gracefully showing the User a nice looking Toaster message.
The question may remains – Where the hell i am handling the Exception? It is handling by the #ExceptionHandler in GlobalExceptionHandlerController .

Spring boot 404 error custom error response ReST

I'm using Spring boot for hosting a REST API. Instead of having the standard error response I would like to always send a JSON response even if a browser is accessing the URL and as well a custom data structure.
I can do this with #ControllerAdvice and #ExceptionHandler for custom exceptions. But I can't find any good ways of doing this for standard and handled errors like 404 and 401.
Are there any good patterns of how to do this?
For those Spring Boot 2 users who don't wanna use #EnableWebMvc
application.properties
server.error.whitelabel.enabled=false
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
ControllerAdvice
#RestControllerAdvice
public class ExceptionResolver {
#ExceptionHandler(NoHandlerFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public HashMap<String, String> handleNoHandlerFound(NoHandlerFoundException e, WebRequest request) {
HashMap<String, String> response = new HashMap<>();
response.put("status", "fail");
response.put("message", e.getLocalizedMessage());
return response;
}
}
Source
It is worked for me in case of #RestControllerAdvice with spring boot
spring.mvc.throw-exception-if-no-handler-found=true
server.error.whitelabel.enabled=false
spring.resources.add-mappings=false
#RestControllerAdvice
public class ErrorHandlerController {
#ExceptionHandler(NoHandlerFoundException.class)
#ResponseStatus(value = HttpStatus.NOT_FOUND )
public String handleNotFoundError(NoHandlerFoundException ex) {
return "path does not exists";
}
}
I've provided the sample solution on how to override response for 404 case. The solution is pretty much simple and I am posting sample code but you can find more details on the original thread: Spring Boot Rest - How to configure 404 - resource not found
First: define Controller that will process error cases and override response:
#ControllerAdvice
public class ExceptionHandlerController {
#ExceptionHandler(NoHandlerFoundException.class)
#ResponseStatus(value= HttpStatus.NOT_FOUND)
#ResponseBody
public ErrorResponse requestHandlingNoHandlerFound() {
return new ErrorResponse("custom_404", "message for 404 error code");
}
}
Second: you need to tell Spring to throw exception in case of 404 (could not resolve handler):
#SpringBootApplication
#EnableWebMvc
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet");
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
}
}
Summing up all answers and comment, I think the best way to do this is-
First, tell spring boot to throw exception in case of no handler found in application.properties
spring.mvc.throw-exception-if-no-handler-found=true
Then handle NoHandlerFoundException in your application. I handle this by following way
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(NoHandlerFoundException.class)
public void handleNotFoundError(HttpServletResponse response, NoHandlerFoundException ex) {
ErrorDto errorDto = Errors.URL_NOT_FOUND.getErrorDto();
logger.error("URL not found exception: " + ex.getRequestURL());
prepareErrorResponse(response, HttpStatus.NOT_FOUND, errorDto);
}
}
If you are using Swagger then you can view my other answer to exclude swagger URL from this exception handler
404 error is handled by DispatcherServlet. there is a property throwExceptionIfNoHandlerFound, which you can override.
In Application class you can create a new bean:
#Bean
DispatcherServlet dispatcherServlet () {
DispatcherServlet ds = new DispatcherServlet();
ds.setThrowExceptionIfNoHandlerFound(true);
return ds;
}
...and then catch the NoHandlerFoundException exception in
#EnableWebMvc
#ControllerAdvice
public class GlobalControllerExceptionHandler {
#ExceptionHandler
#ResponseStatus(value=HttpStatus.NOT_FOUND)
#ResponseBody
public ErrorMessageResponse requestHandlingNoHandlerFound(final NoHandlerFoundException ex) {
doSomething(LOG.debug("text to log"));
}
}
You may extend the ResponseEntityExceptionHandler class, which include a lot of common exceptions in a Spring Boot Project. For example, if you wish to use a custom handler for binding exceptions, you may use the following,
#ControllerAdvice
public class MyApiExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String responseBody = "{\"key\":\"value\"}";
headers.add("Content-Type", "application/json;charset=utf-8");
return handleExceptionInternal(ex, responseBody, headers, HttpStatus.NOT_ACCEPTABLE, request);
}
}
An other example for the http status 404-Not Found,
#ControllerAdvice
public class MyApiExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String responseBody = "{\"errormessage\":\"WHATEVER YOU LIKE\"}";
headers.add("Content-Type", "application/json;charset=utf-8");
return handleExceptionInternal(ex, responseBody, headers, HttpStatus.NOT_FOUND, request);
}
}
Regarding the 404 not found exception you should configure the DispatcherServlet to throw and exception if it doesn't find any handlers, instead of the default behavior. For issues with 404, you may also read this question.
I was having the same issue but fixed it using a different method.
To return 404, 401 and other status in a custom response, you can now add the response status to the custom exception class and call it from your exception handler.
With spring utility class AnnotationUtils, you can get the status of any of the defined custom exceptions with the findAnnotation method and it will return the appropriate status using whatever annotation you defined for the exceptions including not found.
Here's my #RestControllerAdvice
#RestControllerAdvice
public class MainExceptionHandler extends Throwable{
#ExceptionHandler(BaseException.class)
ResponseEntity<ExceptionErrorResponse> exceptionHandler(GeneralMainException e)
{
ResponseStatus status = AnnotationUtils.findAnnotation(e.getClass(),ResponseStatus.class);
if(status != null)
{
return new ResponseEntity<>(new ExceptionErrorResponse(e.getCode(),e.getMessage()),status.code());
}
}
CustomParamsException to return Bad request status
#ResponseStatus(value= HttpStatus.BAD_REQUEST)
public class CustomParamsException extends BaseException {
private static final String CODE = "400";
public CustomParamsException(String message) {
super(CODE, message);
}
}
Details not found to return Not Found Status
#ResponseStatus(value= HttpStatus.NOT_FOUND)
public class DetailsNotException extends BaseException {
private static final String CODE = "400";
public DetailsNotException(String message) {
super(CODE, message);
}
}
A GeneralMainException to extend Excetion
public class GeneralMainException extends Exception {
private String code;
private String message;
public GeneralMainException (String message) {
super(message);
}
public GeneralMainException (String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
#Override
public String getMessage() {
return message;
}
}
You can decide to handle other system exceptions by including it to the controller advice.
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Exception.class)
ExceptionErrorResponse sysError(Exception e)
{
return new ExceptionErrorResponse(""1002", e.getMessage());
}
It seems that you need to introduce an appropriately annotated method, e.g. for unsupported media type (415) it will be:
#ExceptionHandler(MethodArgumentNotValidException)
public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
logger.error('Caught exception', e)
def response = new ExceptionResponse(
error: 'Validation error',
exception: e.class.name,
message: e.bindingResult.fieldErrors.collect { "'$it.field' $it.defaultMessage" }.join(', '),
path: req.servletPath,
status: BAD_REQUEST.value(),
timestamp: currentTimeMillis()
)
new ResponseEntity<>(response, BAD_REQUEST)
}
However it may not be possible since 401 and 404 may be thrown before they reach DispatcherServlet - in this case ControllerAdvice will not work.
You can add custom ErrorPage objects which correlate to the error-page definition in web.xml. Spring Boot provides an example...
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer(){
return new MyCustomizer();
}
// ...
private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthorized.html"));
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/not-found.html"));
}
}
EDIT: While I think the method above will work if you make the error pages rest controllers, an even easier way would be to include a custom ErrorController like the one below...
#Bean
public ErrorController errorController(ErrorAttributes errorAttributes) {
return new CustomErrorController(errorAttributes);
}
// ...
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
#Override
#RequestMapping(value = "${error.path:/error}")
#ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
ResponseEntity<Map<String, Object>> error = super.error(request);
HttpStatus statusCode = error.getStatusCode();
switch (statusCode) {
case NOT_FOUND:
return getMyCustomNotFoundResponseEntity(request);
case UNAUTHORIZED:
return getMyCustomUnauthorizedResponseEntity(request);
default:
return error;
}
}
}
Please see Spring Boot REST service exception handling. It shows how to tell the dispatcherservlet to emit exceptions for "no route found" and then how to catch those exceptions. We (the place I work) are using this in production for our REST services right now.
Starting with Spring version 5 can use class ResponseStatusException:
#GetMapping("example")
public ResponseEntity example() {
try {
throw new MyException();
} catch (MyException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "My Exception", e);
}
}
I wanted to have the same error format (json) structure across all possible error scenarios, so I just registered my own ErrorController reusing the code from AbstractErrorController:
#Controller
#RequestMapping(path = "/error", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public class ErrorController extends AbstractErrorController {
public ErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers.orderedStream().collect(Collectors.toUnmodifiableList()));
}
#RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
final var status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
return new ResponseEntity<>(getErrorAttributes(request, ErrorAttributeOptions.defaults()), status);
}
#Override
public String getErrorPath() {
return null;
}
}
with this you dont need any controller advice, all errors go to error method by default

Categories

Resources