21

I am handling exceptions in spring using @ExceptionHandler. Any exception thrown by controller is caught using method annotated with @ExceptionHandler and action is taken accordingly. To avoid writing @exceptionHandler for every controller i am using @ControllerAdvice annotation.

Everything works fine as expected.

Now i have a filter(Yes, not interceptor, to handle certain requirement) which is implemented using DelegatingFilterProxy and ContextLoaderListener.

When i am throwing the same exception from above filter, its not caught the way it was done in controller case. It is directly thrown to user.

What's wrong in here?

Rohit Jain
  • 341
  • 2
  • 3
  • 16
  • Also check this http://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring – kopelitsa Jan 06 '16 at 16:26
  • See here https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring/43242424, I did that workaround to make use of ```@ExceptionHanlder``` for a ```Exception``` that was thrown in a ```Filter``` – Raf Apr 05 '17 at 22:09

8 Answers8

29

Filters happens before controllers are even resolved so exceptions thrown from filters can't be caught by a Controller Advice.

Filters are a part of the servlet and not really the MVC stack.

Andreas Wederbrand
  • 38,065
  • 11
  • 68
  • 78
  • 3
    I suppose so. Can there be any get around for this? We can autowire spring beans in filter, this means we can make use of spring DI, not the whole stack but some features of it. One workaround (should avoid though), in case of exception, filter catches them and throw to a controller that in turns throws exception. This is just to maintain consistency. – Rohit Jain Jul 18 '13 at 09:04
  • Perhaps you could create a filter with index 1 (first filter) that catches all exceptions an manually triggers the exceptionhandler that the controllers use. – Andreas Wederbrand Jul 18 '13 at 12:53
  • Can you throw more light on "manually triggers the exceptionhandler that controller use"? Does it means actually calling a method of controller that throws the required exception. – Rohit Jain Jul 18 '13 at 15:49
  • Your controllerAdvice handles exceptions today, automatically through spring, so I was thinking that perhaps you could autowire in the controllerAdvice into that filter and just call the handle-method "manually". I haven't tried this but it's worth a shot. You'll get the same exception handling from controllers and filters. – Andreas Wederbrand Jul 18 '13 at 17:57
  • Insightful but not helpful, hence the downvote. Richards answer below provide less context but a working solution. – joerx Oct 19 '19 at 14:30
  • what if we make the highest precedence order of Controler advice? – AmeeQ Jun 15 '23 at 05:40
16

As the exception is not raised from a controller but a filter, @ControllerAdvice won't catch it.

So, the best solution i found after looking everywhere was to create a filter for this internal errors:

public class ExceptionHandlerFilter extends OncePerRequestFilter {
    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);

        } catch (JwtException e) {
            setErrorResponse(HttpStatus.BAD_REQUEST, response, e);
            e.printStackTrace();
        } catch (RuntimeException e) {
            e.printStackTrace();
            setErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, response, e);
        }
    }

    public void setErrorResponse(HttpStatus status, HttpServletResponse response, Throwable ex){
        response.setStatus(status.value());
        response.setContentType("application/json");
        // A class used for errors
        ApiError apiError = new ApiError(status, ex);
        try {
            String json = apiError.convertToJson();
            System.out.println(json);
            response.getWriter().write(json);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Add it to your config, i'm using a WebSecurityConfigurerAdapter implementation:

// Custom JWT based security filter
httpSecurity
        .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

// Custom Exception Filter for filter
httpSecurity
        .addFilterBefore(exceptionHandlerFilterBean(), JwtAuthenticationTokenFilter.class);

The error class:

public class ApiError {

    private HttpStatus status;
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    private LocalDateTime timestamp;
    private String message;
    private String debugMessage;

    private ApiError() {
        timestamp = LocalDateTime.now();
    }
    public ApiError(HttpStatus status) {
        this();
        this.status = status;
    }

    public ApiError(HttpStatus status, Throwable ex) {
        this();
        this.status = status;
        this.message = "Unexpected error";
        this.debugMessage = ex.getLocalizedMessage();
    }

    public ApiError(HttpStatus status, String message, Throwable ex) {
        this();
        this.status = status;
        this.message = message;
        this.debugMessage = ex.getLocalizedMessage();
    }

    public String convertToJson() throws JsonProcessingException {
        if (this == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        return mapper.writeValueAsString(this);
    }

  //Setters and getters
}
Richard
  • 719
  • 10
  • 11
  • Does `if (this == null)` in ApiError has any sense? I think `this` can never be `null` as this answer states: https://stackoverflow.com/a/3789534/9246862 – Smile Mar 11 '22 at 10:50
4

Since the exception is not thrown by a controller, the controller advice won't catch the exception unless you provide a custom filter to delegate your exception.

You can create another Filter to delegate your exceptions to the controller advice. The trick is to provide this newly created filter before all other custom filters.'

For eg:

  1. Create a new Filter to delegate your Exception

    @Component
    public class FilterExceptionHandler extends OncePerRequestFilter {
    
    private static Logger logger = LoggerFactory.getLogger(FilterExceptionHandler.class);
    
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;
    
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } catch (Exception ex) {
            logger.error("Spring Security filter chain exception : {}", ex.getMessage());
            resolver.resolveException(httpServletRequest, httpServletResponse, null, ex);
        }
    }}
    
  2. Create a custom exception if you need. In my case I'm creating an exception JukeBoxUnAuthorizedException

    public class JukeBoxUnauthorizedException extends RuntimeException {
      private static final long serialVersionUID = 3231324329208948384L;
      public JukeBoxUnauthorizedException() {
        super();
      }
    
      public JukeBoxUnauthorizedException(String message) {
        super(message);
      }
    
      public JukeBoxUnauthorizedException(String message, Throwable cause) {
        super(message, cause);
      }
    }
    
  3. Create a Controller Advice which would handle this exception

    @Order(Ordered.HIGHEST_PRECEDENCE)
    @ControllerAdvice
    public class RestExceptionHandler  {
      @ExceptionHandler(value = {JukeBoxUnauthorizedException.class})
      public ResponseEntity<JukeboxResponse> handleUnAuthorizedException(JukeBoxUnauthorizedException exception) {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(exception.getMessage()));
      }
    }
    
  4. Add your exception delegation filter in SecurityConfigurtion. i.e in the configure(HttpSecurity http) method . please note that the exception delegating filter should be in the top of the hierarchy. It should be before all your custom filters

http.addFilterBefore(exceptionHandlerFilter, AuthTokenFilter.class);

Fredrik
  • 10,626
  • 6
  • 45
  • 81
emilpmp
  • 1,716
  • 17
  • 32
  • HandleExceptionResolver is always null for me. – BBacon Jun 15 '21 at 19:56
  • @BBacon You can also try using SimpleMappingExceptionResolver as explained here - https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc – emilpmp Jun 16 '21 at 07:23
3

Presumably, you want to set the HTTP Status code as a result of the exception being thrown in the Filter? If so, simply set the status as follows:

HttpServletResponse response = (HttpServletResponse) res; response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

2

This is what I did in my filter class to throw error:

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        if (req.getHeader("Content-Type") == null) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;                
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                   "Required headers not specified in the request");            
        }
        chain.doFilter(request, response);
    }
JimHawkins
  • 4,843
  • 8
  • 35
  • 55
DPancs
  • 588
  • 6
  • 14
1

I built my application with rest api, so I resolved this problem by catching it in the filter that may throw an exception and then writing something back. Remember that filterChain.doFilter(request, response); must be included.

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        // something may throw an exception
        filterChain.doFilter(request, response);
    } catch (Exception e) {
        // ResponseWrapper is a customized class
        ResponseWrapper responseWrapper = new ResponseWrapper().fail().msg(e.getMessage());
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(JSON.toJSONString(responseWrapper));
    }
}
xiaofeig
  • 131
  • 1
  • 4
0

Check the below code snippet, it works for me.

final HttpServletResponseWrapper wrapper = new 
HttpServletResponseWrapper((HttpServletResponse) res);    
wrapper.sendError(HttpServletResponse.SC_UNAUTHORIZED, "<your error msg>");    
res = wrapper.getResponse();

If you are using this inside a filter then add a return statement else chain.doFilter(req,res) will override this.

Sumner Evans
  • 8,951
  • 5
  • 30
  • 47
user3484940
  • 371
  • 3
  • 2
  • Hey, I have editted this code, but name shown of other user. Actually I editted the code without login. but after login it is showing @Sumner Evans name. – Vijay Shegokar Apr 07 '17 at 05:47
  • @vijayshegokar, I accepted your edit, but made a few extra improvements. (See the [full edit history](http://stackoverflow.com/posts/36138972/revisions)). – Sumner Evans Apr 07 '17 at 05:49
  • @SumnerEvans, Thanks for the quick reply and accepting my edit. But still I think my name should be displayed here as I had edited the answer. :) – Vijay Shegokar Apr 07 '17 at 06:08
  • @vijayshegokar, the "edited x minutes" ago spot only shows who made the latest edit. That's just how StackOverflow works. There are some posts on SO that have 5+ collaborators. If all of them were put on those posts, they would be unreadable for all the editor profile pics. – Sumner Evans Apr 07 '17 at 06:18
0

If, like me, you're stuck with spring 3.1 (just 0.1 vesrsions behind @ControllerAdvice) you can try this solution I just came up with.


So, you've heard of exception resolvers, right? If not, read here:

@Component
public class RestExceptionResolver extends ExceptionHandlerExceptionResolver {

    @Autowired
    //If you have multiple handlers make this a list of handlers
    private RestExceptionHandler restExceptionHandler;
    /**
     * This resolver needs to be injected because it is the easiest (maybe only) way of getting the configured MessageConverters
     */
    @Resource
    private ExceptionHandlerExceptionResolver defaultResolver;

    @PostConstruct
    public void afterPropertiesSet() {
        setMessageConverters(defaultResolver.getMessageConverters());
        setOrder(2); // The annotation @Order(2) does not work for this type of component
        super.afterPropertiesSet();
    }

    @Override
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
        ExceptionHandlerMethodResolver methodResolver = new ExceptionHandlerMethodResolver(restExceptionHandler.getClass());
        Method method = methodResolver.resolveMethod(exception);
        if (method != null) {
            return new ServletInvocableHandlerMethod(restExceptionHandler, method);
        }
        return null;
    }

    public void setRestExceptionHandler(RestExceptionHandler restExceptionHandler) {
        this.restExceptionHandler = restExceptionHandler;
    }

    public void setDefaultResolver(ExceptionHandlerExceptionResolver defaultResolver) {
        this.defaultResolver = defaultResolver;
    }
}

Then an example handler will look like this

@Component
public class RestExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public Map<String, Object> handleException(ResourceNotFoundException e, HttpServletResponse response) {
        Map<String, Object> error = new HashMap<>();
        error.put("error", e.getMessage());
        error.put("resource", e.getResource());
        return error;
    }
 }

Of course you will not forget to register your beens


Then create a filter that is called before your desiered filter (optionally all of 'em)

Then in that filter

try{
   chain.doFilter(request, response);
catch(Exception e){
   exceptionResolver(request, response, exceptionHandler, e);
   //Make the processing stop here... 
   return; //just in case
}
Panda
  • 6,955
  • 6
  • 40
  • 55
Amanuel Nega
  • 1,899
  • 1
  • 24
  • 42