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