Related
I am using both the ExceptionAdvice and ResponseStatusException to handle the exception situation in my web application. Now I'd like to log the exception information while throwing the ResponseStatusException in my Controller class.
I can always write the log code near the line that throw the exception in my Controller class:
controllerMethod(){
logger.error("some thing happens here!");
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "some reason");
}
But writing code all over the place is too tedious, In fact, i'd like some pattern that i used in my ExceptionAdvice class:
#ResponseBody
#ExceptionHandler(MyException.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
String myExceptionHandler(MyException e){
logger.error("oops!", e);
return "something";
}
However, the ReponseStatusException response that Spring generated has the format which i want to maintain like:
{
"timestamp": "2018-02-01T04:28:32.917+0000",
"status": 400,
"error": "Bad Request",
"message": "Provide correct Actor Id",
"path": "/actor/8/BradPitt"
}
So is there anyway that i can use advice class to log for the ResponseStatusException while still maintaining its generated response, or, on the contrast, using other class to add log ability around all the ReponseStatusException without typing the logger.error everywhere that the exception is thrown?
You can enable the logging with the property
spring.mvc.log-resolved-exception=true
involved classes are
WebMvcAutoConfiguration
AbstractHandlerExceptionResolver
ResponseStatusExceptionResolver
Here's one way to do it (tested in Spring Boot 2.3.3).
Create a class that extends ResponseStatusExceptionResolver:
/**
* Extended implementation that adds logging of {#link ResponseStatusException}s.
* <p>
* Note: {#link #setMessageSource(MessageSource)} has to be called if reason is a message code, rather than a message itself
*/
private static class LoggingResponseStatusExceptionResolver extends ResponseStatusExceptionResolver {
#Override
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log(responseStatus.code(), responseStatus.reason(), ex);
return super.resolveResponseStatus(responseStatus, request, response, handler, ex);
}
#Override
protected ModelAndView resolveResponseStatusException(ResponseStatusException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log(ex.getStatus(), ex.getReason(), ex);
return super.resolveResponseStatusException(ex, request, response, handler);
}
private void log(HttpStatus status, String reason, Exception exception) {
if (status.isError()) {
logger.error(status + ": " + reason + "\n", exception);
}
}
}
Then in one of your application configuration classes implement WebMvcConfigurer and replace the default resolver with the extended one:
#Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.replaceAll(resolver ->
resolver instanceof ResponseStatusExceptionResolver
? new LoggingResponseStatusExceptionResolver()
: resolver
);
}
I've written a few extensions of ExceptionHandlerExceptionResolver, it intercepts all exceptions that it should, but instead of returning only error message and HTTP status code it makes really weird redirect by its own URL built upon users requested URL. For instance:
user's url -> .../myModule/api/myEntity/123 (it's an id)
resolver's redirect url -> .../myModule/api/myEntity/myEntity/123
Server doesn't have such resource and obviously it will respond with 404.
The question is: why it makes redirect and how to configure it to return only a message and status code?
My resolver:
public class BusinessLayerExceptionHandler extends ExceptionHandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView wrappedResponse = new ModelAndView();
wrappedResponse.addObject("errorMessage", ex.getMessage());
wrappedResponse.setStatus(HttpStatus.BAD_REQUEST);
return wrappedResponse;
}
}
I guess the usage of ModelAndView assumes redirection. At least that's a method description that I found in DispatcherServlet.
...
* #return a corresponding ModelAndView to forward to
* #throws Exception if no error ModelAndView found
*/
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
...
If so, how to make it return just error message and HTTP status code?
You can return just error message and HTTP status code by creating a custom View.
public class YourCustomView implements View {
private final String errorMessage;
public YourCustomView(String errorMessage) {
this.errorMessage = errorMessage;
}
#Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType("text/plain;charset=UTF-8");
try (PrintWriter pw = response.getWriter()) {
pw.write(errorMessage);
}
}
}
You need to put the custom View object into ModelAndView object in HandlerExceptionResolver#resolveException.
public class BusinessLayerExceptionHandler implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
ModelAndView wrappedResponse = new ModelAndView();
wrappedResponse.setStatus(HttpStatus.BAD_REQUEST);
wrappedResponse.setView(new YourCustomView(ex.getMessage()));
return wrappedResponse;
}
}
why it makes redirect
It seems that Spring recognize the view name as a defaultViewName and forwards to it (by calling RequestDispatcher#forward).
In DispatcherServlet#processHandlerException, a defaultViewName is set to the view name of a ModelAndView returned by resolveException when it doesn't have View object. A defaultViewName is got from DispatcherServlet#getDefaultViewName that translates a HTTP request into a view name.
Another Solution
I think you may be able to use #ControllerAdvice and #ExceptionHandler instead. It also can handle an exception thrown from a controller.
#ControllerAdvice
public class YourControllerAdvice {
#ExceptionHandler
public ResponseEntity<Map<String, String>> handleBusinessLayerException(
Exception exception) {
Map<String, String> body = Map.of("errorMessage", exception.getMessage());
return ResponseEntity.badRequest().body(body);
}
}
See Also
Spring Web MVC document about HandlerExceptionResolver
Spring Web MVC document about ControllerAdvice
I'm trying to use Spring's HandlerInterceptorAdapter to handle the case when the application is scheduled for Maintenance for the following rest-endpoint: /api/authentication
So I created an interceptor by extending HandlerInterceptorAdapter:
public class MigrationStateInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(someLogic) {
return true
}
response.reset();
response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
return false;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
And then I added it to my InterceptorRegistry:
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MigrationStateInterceptor()).addPathPatterns("/api/authentication");
}
The problem is that instead of receiving a 503 (Service Unavailable) error on client-side, I receive Failed to load resource: the server responded with a status of 406 (Not Acceptable)
As it can be seen in the code snippet, I tried reseting the response, but with no result.
I also tried modifying the Accept and Content-Type headers but with no luck:
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.addHeader("Accept", "text/plain");
Any idea why this occurs and how it can be avoided?
NOTE: I'm using Angular on client-side. From what I see HttpServletResponse#sendError sets the content type to text/html, leaving cookies and other headers unmodified. Could this be a problem?
EDIT:
I even tried throwing an exception instead of using HttpServletResponse#sendError, and handle that separately, but the end result was the same.
E.g.:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (some logic) {
return true;
}
throw new MigrationStateException("Migration process is disabled");
}
#ExceptionHandler(MigrationStateException.class)
public ResponseEntity<String> migrationStateError(MigrationStateException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.SERVICE_UNAVAILABLE);
}
I believe the problem is the request you're mapping to /api/authenticate is either not expecting text/html, or your sending the wrong type of request (i.e. GET instead of a POST, etc.)
You stated that angular is sending text/html, given the usual defaults associated with an authenticate endpoint, it's probably expecting application/x-www-form-urlencoded data. Therefore, Spring is intercepting a Content-Type it doesn't have mapped and 406'ing.
I want to use generic way to manage 5xx error codes, let's say specifically the case when the db is down across my whole spring application. I want a pretty error json instead of a stack trace.
For the controllers I have a #ControllerAdvice class for the different exceptions and this is also catching the case that the db is stopping in the middle of the request. But this is not all. I also happen to have a custom CorsFilter extending OncePerRequestFilter and there when i call doFilter i get the CannotGetJdbcConnectionException and it will not be managed by the #ControllerAdvice. I read several things online that only made me more confused.
So I have a lot of questions:
Do i need to implement a custom filter? I found the ExceptionTranslationFilter but this only handles AuthenticationException or AccessDeniedException.
I thought of implementing my own HandlerExceptionResolver, but this made me doubt, I don't have any custom exception to manage, there must be a more obvious way than this. I also tried to add a try/catch and call an implementation of the HandlerExceptionResolver (should be good enough, my exception is nothing special) but this is not returning anything in the response, i get a status 200 and an empty body.
Is there any good way to deal with this? Thanks
So this is what I did:
I read the basics about filters here and I figured out that I need to create a custom filter that will be first in the filter chain and will have a try catch to catch all runtime exceptions that might occur there. Then i need to create the json manually and put it in the response.
So here is my custom filter:
public class ExceptionHandlerFilter extends OncePerRequestFilter {
#Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (RuntimeException e) {
// custom error response class used across my project
ErrorResponse errorResponse = new ErrorResponse(e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(convertObjectToJson(errorResponse));
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
And then i added it in the web.xml before the CorsFilter. And it works!
<filter>
<filter-name>exceptionHandlerFilter</filter-name>
<filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>exceptionHandlerFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
I wanted to provide a solution based on the answer of #kopelitsa. The main differences being:
Reusing the controller exception handling by using the HandlerExceptionResolver.
Using Java config over XML config
First, you need to make sure, that you have a class that handles exceptions occurring in a regular RestController/Controller (a class annotated with #RestControllerAdvice or #ControllerAdvice and method(s) annotated with #ExceptionHandler). This handles your exceptions occurring in a controller. Here is an example using the RestControllerAdvice:
#RestControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(RuntimeException.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorDTO processRuntimeException(RuntimeException e) {
return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
}
private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
(...)
}
}
To reuse this behavior in the Spring Security filter chain, you need to define a Filter and hook it into your security configuration. The filter needs to redirect the exception to the above defined exception handling. Here is an example:
#Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {
private final Logger log = LoggerFactory.getLogger(getClass());
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
The created filter then needs to be added to the SecurityConfiguration. You need to hook it into the chain very early, because all preceding filter's exceptions won't be caught. In my case, it was reasonable to add it before the LogoutFilter. See the default filter chain and its order in the official docs. Here is an example:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
(...)
}
}
I come across this issue myself and I performed the steps below to reuse my ExceptionController that is annotated with #ControllerAdvise for Exceptions thrown in a registered Filter.
There are obviously many ways to handle exception but, in my case, I wanted the exception to be handled by my ExceptionController because I am stubborn and also because I don't want to copy/paste the same code (i.e. I have some processing/logging code in ExceptionController). I would like to return the beautiful JSON response just like the rest of the exceptions thrown not from a Filter.
{
"status": 400,
"message": "some exception thrown when executing the request"
}
Anyway, I managed to make use of my ExceptionHandler and I had to do a little bit of extra as shown below in steps:
Steps
You have a custom filter that may or may not throw an exception
You have a Spring controller that handles exceptions using #ControllerAdvise i.e. MyExceptionController
Sample code
//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
//Spring Controller annotated with #ControllerAdvise which has handlers
//for exceptions
private MyExceptionController myExceptionController;
#Override
public void destroy() {
// TODO Auto-generated method stub
}
#Override
public void init(FilterConfig arg0) throws ServletException {
//Manually get an instance of MyExceptionController
ApplicationContext ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(arg0.getServletContext());
//MyExceptionHanlder is now accessible because I loaded it manually
this.myExceptionController = ctx.getBean(MyExceptionController.class);
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
try {
//code that throws exception
} catch(Exception ex) {
//MyObject is whatever the output of the below method
MyObject errorDTO = myExceptionController.handleMyException(req, ex);
//set the response object
res.setStatus(errorDTO .getStatus());
res.setContentType("application/json");
//pass down the actual obj that exception handler normally send
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = res.getWriter();
out.print(mapper.writeValueAsString(errorDTO ));
out.flush();
return;
}
//proceed normally otherwise
chain.doFilter(request, response);
}
}
And now the sample Spring Controller that handles Exception in normal cases (i.e. exceptions that are not usually thrown in Filter level, the one we want to use for exceptions thrown in a Filter)
//sample SpringController
#ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {
//sample handler
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ExceptionHandler(SQLException.class)
public #ResponseBody MyObject handleSQLException(HttpServletRequest request,
Exception ex){
ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
+ "executing the request.");
return response;
}
//other handlers
}
Sharing the solution with those who wish to use ExceptionController for Exceptions thrown in a Filter.
So, here's what I did based on an amalgamation of the above answers... We already had a GlobalExceptionHandler annotated with #ControllerAdvice and I also wanted to find a way to re-use that code to handle exceptions that come from filters.
The simplest solution I could find was to leave the exception handler alone, and implement an error controller as follows:
#Controller
public class ErrorControllerImpl implements ErrorController {
#RequestMapping("/error")
public void handleError(HttpServletRequest request) throws Throwable {
if (request.getAttribute("javax.servlet.error.exception") != null) {
throw (Throwable) request.getAttribute("javax.servlet.error.exception");
}
}
}
So, any errors caused by exceptions first pass through the ErrorController and are re-directed off to the exception handler by rethrowing them from within a #Controller context, whereas any other errors (not caused directly by an exception) pass through the ErrorController without modification.
Any reasons why this is actually a bad idea?
If you want a generic way, you can define an error page in web.xml:
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/500</location>
</error-page>
And add mapping in Spring MVC:
#Controller
public class ErrorController {
#RequestMapping(value="/500")
public #ResponseBody String handleException(HttpServletRequest req) {
// you can get the exception thrown
Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");
// customize response to what you want
return "Internal server error.";
}
}
This is my solution by overriding default Spring Boot /error handler
package com.mypackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* This controller is vital in order to handle exceptions thrown in Filters.
*/
#RestController
#RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {
private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);
private final ErrorAttributes errorAttributes;
#Autowired
public ErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return "/error";
}
#RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
Map<String, Object> result = this.errorAttributes.getErrorAttributes(requestAttributes, false);
Throwable error = this.errorAttributes.getError(requestAttributes);
ResponseStatus annotation = AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;
result.put("status", statusCode.value());
result.put("error", statusCode.getReasonPhrase());
LOGGER.error(result.toString());
return new ResponseEntity<>(result, statusCode) ;
}
}
Just to complement the other fine answers provided, as I too recently wanted a single error/exception handling component in a simple SpringBoot app containing filters that may throw exceptions, with other exceptions potentially thrown from controller methods.
Fortunately, it seems there is nothing to prevent you from combining your controller advice with an override of Spring's default error handler to provide consistent response payloads, allow you to share logic, inspect exceptions from filters, trap specific service-thrown exceptions, etc.
E.g.
#ControllerAdvice
#RestController
public class GlobalErrorHandler implements ErrorController {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ValidationException.class)
public Error handleValidationException(
final ValidationException validationException) {
return new Error("400", "Incorrect params"); // whatever
}
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Exception.class)
public Error handleUnknownException(final Exception exception) {
return new Error("500", "Unexpected error processing request");
}
#RequestMapping("/error")
public ResponseEntity handleError(final HttpServletRequest request,
final HttpServletResponse response) {
Object exception = request.getAttribute("javax.servlet.error.exception");
// TODO: Logic to inspect exception thrown from Filters...
return ResponseEntity.badRequest().body(new Error(/* whatever */));
}
#Override
public String getErrorPath() {
return "/error";
}
}
When you want to test a state of application and in case of a problem return HTTP error I would suggest a filter. The filter below handles all HTTP requests. The shortest solution in Spring Boot with a javax filter.
In the implementation can be various conditions. In my case the applicationManager testing if the application is ready.
import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Component
public class SystemIsReadyFilter implements Filter {
#Autowired
private ApplicationManager applicationManager;
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!applicationManager.isApplicationReady()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
} else {
chain.doFilter(request, response);
}
}
#Override
public void destroy() {}
}
After reading through different methods suggested in the above answers, I decided to handle the authentication exceptions by using a custom filter. I was able to handle the response status and codes using an error response class using the following method.
I created a custom filter and modified my security config by using the addFilterAfter method and added after the CorsFilter class.
#Component
public class AuthFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//Cast the servlet request and response to HttpServletRequest and HttpServletResponse
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// Grab the exception from the request attribute
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
//Set response content type to application/json
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
//check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
// If exception instance cannot be determined, then throw a nice exception and desired response code.
else if(exception!=null){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
else {
// proceed with the initial request if no exception is thrown.
chain.doFilter(httpServletRequest,httpServletResponse);
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
SecurityConfig class
#Configuration
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
AuthFilter authenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
.cors(); //........
return http;
}
}
ErrorResponse class
public class ErrorResponse {
private final String message;
private final String description;
public ErrorResponse(String description, String message) {
this.message = message;
this.description = description;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}}
You can use the following method inside the catch block:
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")
Notice that you can use any HttpStatus code and a custom message.
I had the same issue in webflux, going on the theme that someone is looking to resuse there #ControllerAdvice, you do not want to throw a direct exception or return a mono error in the webfilter, however you want to set the response to be the mono error.
public class YourFilter implements WebFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
exchange.getResponse().writeWith(Mono.error(new YouException()));
return chain.filter(exchange)
}
}
In Filters, we don't have a control with #ControllerAdvice or #RestControllerAdvice to handle our exceptions that could occur at the time of doing the authentication. Because, DispatcherServlet will only come into picture after the Controller class hits.
So, we need to do the following.
we need to have
HttpServletResponse httpResponse = (HttpServletResponse) response;
"response" object we can pass it from public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) of GenericFilterBean.java implementation class.
2) We can use the below utility class to write or print our error JSON model or String object into the ServletResponse output stream.
public static void handleUnAuthorizedError(ServletResponse response,Exception e)
{
ErrorModel error = null;
if(e!=null)
error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, e.getMessage());
else
error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, ApplicationConstants.UNAUTHORIZED);
JsonUtils jsonUtils = new JsonUtils();
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
httpResponse.getOutputStream().println(jsonUtils.convertToJSON(error));
} catch (IOException ex) {
ex.printStackTrace();
}
}
public String convertToJSON(Object inputObj) {
ObjectMapper objectMapper = new ObjectMapper();
String orderJson = null;
try {
orderJson = objectMapper.writeValueAsString(inputObj);
}
catch(Exception e){
e.printStackTrace();
}
return orderJson;
}
Late to the party but we can also use it like this:
#ApiIgnore
#RestControllerAdvice
public class ExceptionHandlerController {
#Autowired
private MessageSource messageSource;
And in the filter:
#Component
public class MyFilter extends OncePerRequestFilter {
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver exceptionResolver;
#Override
protected void doFilterInternal(HttpServletRequest request, #NotNull HttpServletResponse response, #NotNull FilterChain filterChain) {
try {
// Some exception
} catch (Exception e) {
this.exceptionResolver.resolveException(request, response, null, e);
}
}
Global Default Exception Handlers will work only at Controller or Service level. They will not work at filter level. I found below solution working fine with Spring Boot Security - JWT filter
https://www.jvt.me/posts/2022/01/17/spring-servlet-filter-error-handling/
Below is the snippet I added
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.getWriter().write("{\"error\":\"invalid_token\",\"error_description\":\"Invalid Token\"}");
You do not need to create a custom Filter for this. We solved this by creating custom exceptions that extend ServletException (which is thrown from the doFilter method, shown in the declaration). These are then caught and handled by our global error handler.
edit: grammar
It's strange because #ControllerAdvice should works, are you catching the correct Exception?
#ControllerAdvice
public class GlobalDefaultExceptionHandler {
#ResponseBody
#ExceptionHandler(value = DataAccessException.class)
public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
}
Also try to catch this exception in CorsFilter and send 500 error, something like this
#ExceptionHandler(DataAccessException.class)
#ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
I have a method in controller with has parameter for example
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public void post(#RequestHeader("ETag") int etag)
If there is no ETag header in request - client gets 400 (BAD_REQUEST), which is not any informative.
I need to somehow handle this exception and send my own exception to client (I use JSON for this purpose).
I know that I can intercept exception via #ExceptionHandler, but in that case all HTTP 400 requests will be handled, but I want that have missing ETag in headers.
Any ideas?
You can also achieve this by use of annotation #ControllerAdvice from spring.
#ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler{
/**
* Handle ServletRequestBindingException. Triggered when a 'required' request
* header parameter is missing.
*
* #param ex ServletRequestBindingException
* #param headers HttpHeaders
* #param status HttpStatus
* #param request WebRequest
* #return the ResponseEntity object
*/
#Override
protected ResponseEntity<Object> handleServletRequestBindingException(ServletRequestBindingException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(ex.getMessage(), headers, status);
}
}
The response when you access your API without the required request header is:
Missing request header 'Authorization' for method parameter of type String
Like this exception, you can customise all other exceptions.
In case Spring version is 5+ then the exact exception you need to handle is the MissingRequestHeaderException. If your global exception handler class extends ResponseEntityExceptionHandler then adding an #ExceptionHandler for ServletRequestBindingException won't work because MissingRequestHeaderException extends ServletRequestBindingException and the latter is handled inside the handleException method of the ResponseEntityExceptionHandler. If you try you're going to get Ambiguous #ExceptionHandler method mapped for ... exception.
There are two ways to achieve what you are trying
First using #RequestHeader with required false
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public void post(#RequestHeader(value="ETag", required=false) String ETag) {
if(ETag == null) {
// Your JSON Error Handling
} else {
// Your Processing
}
}
Second using HttpServletRequest instead of #RequestHeader
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public void post(HttpServletRequest request) {
String ETag = request.getHeader("ETag");
if(ETag == null) {
// Your JSON Error Handling
} else {
// Your Processing
}
}
Write a method with the annotation #ExceptionHandler and use ServletRequestBindingException.class as this exception is thrown in case of missing header
For example :
#ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ResponseObject> handleHeaderError(){
ResponseObject responseObject=new ResponseObject();
responseObject.setStatus(Constants.ResponseStatus.FAILURE.getStatus());
responseObject.setMessage(header_missing_message);
ResponseEntity<ResponseObject> responseEntity=new ResponseEntity<ResponseObject>(responseObject, HttpStatus.BAD_REQUEST);
return responseEntity;
}
In Spring 5+ it is as simple as this. ErrorResponse is your own object to return
#RestControllerAdvice
public class ControllerExceptionHandler {
#ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity<ErrorResponse> handleException(MissingRequestHeaderException ex) {
log.error("Error due to: " + ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse();
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
You should user an #ExceptionHandler method that looks if ETag header is present and takes appropriate action :
#ExceptionHandler(UnsatisfiedServletRequestParameterException.class)
public onErr400(#RequestHeader(value="ETag", required=false) String ETag,
UnsatisfiedServletRequestParameterException ex) {
if(ETag == null) {
// Ok the problem was ETag Header : give your informational message
} else {
// It is another error 400 : simply say request is incorrect or use ex
}
}
If you don't want to handle this in your request mapping, then you could create a Servlet Filter and look for the ETag header in the Filter. If it's not there, then throw the exception. This would apply to only requests that match your filter's URL mapping.
public final class MyEtagFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String etag = request.getHeader("ETag");
if(etag == null)
throw new MissingEtagHeaderException("...");
filterChain.doFilter(request, response);
}
}
You'll have to implement your own MissingEtagHeaderException, or use some other existing exception.
This is relatively simple. Declare two handler methods, one that declares the appropriate header in the #RequestMapping headers attribute and one that doesn't. Spring will take care to invoke the appropriate one based on the content of the request.
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST, headers = "ETag")
#ResponseStatus(HttpStatus.CREATED)
public void postWith(#RequestHeader("ETag") int etag) {
// has it
}
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public void postWithout() {
// no dice
// custom failure response
}
You can also intercept the exception without extending ResponseEntityExceptionHandler:
#ControllerAdvice
public class ControllerExceptionHandler {
#ExceptionHandler(ServletRequestBindingException.class)
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleServletRequestBindingException(ServletRequestBindingException ex) {
// return a ResponseEntity<Object> object here.
}
}
You can add #Nullable to this request param, and in case of absence, request still enters the controller without throwing MissingRequestHeaderException, and you add manual validation to throw whatever you like in controller and handle in the ExceptionHandler.
You can create a custom exception class e.g. InvalidRequestHeaderException.java. You can customise your exception message here.
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidRequestHeaderException extends RuntimeException {
public InvalidRequestHeaderException() {
super("Invalid request header provided.");
}
}
In your controller, you can throw an exception if the header provided is invalid.
#RequestMapping(value = "/{blabla}", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public void post(#RequestHeader("ETag") int etag) {
// some code
if (!isSupportedPlatform(platform)) {
throw new InvalidRequestHeaderException();
}
// some code
}
You can then create a ValidationHandler.java to handle these exceptions.
#RestControllerAdvice
public class ValidationHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {
MissingRequestHeaderException.class,
InvalidRequestHeaderException.class
})
protected ResponseEntity<Object> handleRequestHeaderException(Exception ex) {
log.error(ex.getMessage());
return ResponseEntity.badRequest().body(ErrorResponse.builder()
.status(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.reason(ex.getMessage()).build());
}
#AllArgsConstructor
#Getter
#Builder
public static class ErrorResponse {
private String status;
private String reason;
}
}
By using MissingRequestHeaderException, it will throw an exception if what you've annotated with #RequestHeader is missing, so you will get an exception like this:
Missing request header 'Etag' for method parameter of type int
And when the request header is present but not valid this exception will be thrown:
Invalid request header provided.