Rohit Jain
Rohit Jain

Reputation: 351

exception handling for filter in spring

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?

Upvotes: 22

Views: 37220

Answers (9)

Michail Michailidis
Michail Michailidis

Reputation: 12181

I had the same issue and when my JwtFilter was failing to validate jwt it would result in 403 or even 500 and I wanted to be 401. I was trying to catch BadCredentialsException in my global @ControllerAdvice exception handler but with no results..

So the proper way instead of calling response.getWriter() that can mess up other things that want to write afterwards (we had this issue ourselves since the writer was already called and the result didn't have any extra info), is to introduce an JwtAuthEntryPoint if your case is about jwt related exceptions that happen in JwtFilter

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // Respond with 401 Unauthorized error
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Invalid or missing JWT token");
    }
}

and bind it in your web security config like that:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthEntryPoint jwtAuthEntryPoint;
    private final JwtRequestFilter jwtRequestFilter;

    public SecurityConfig(JwtAuthEntryPoint jwtAuthEntryPoint, JwtRequestFilter jwtRequestFilter) {
        this.jwtAuthEntryPoint = jwtAuthEntryPoint;
        this.jwtRequestFilter = jwtRequestFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthEntryPoint)) 
             .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
    [...]
}

Upvotes: 0

emilpmp
emilpmp

Reputation: 1736

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

Upvotes: 4

DPancs
DPancs

Reputation: 617

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

Upvotes: 2

xiaofeig
xiaofeig

Reputation: 151

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

Upvotes: 1

Richard
Richard

Reputation: 769

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
}

Upvotes: 18

Amanuel Nega
Amanuel Nega

Reputation: 1977

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
}

Upvotes: 0

user3484940
user3484940

Reputation: 371

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.

Upvotes: 0

darren rose
darren rose

Reputation: 31

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

Upvotes: 3

Andreas Wederbrand
Andreas Wederbrand

Reputation: 39981

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.

Upvotes: 32

Related Questions