2
votes

How can I override the default output of Spring Boot's error handling? When a 403 response or something like that happens I want to change the default that is shown.

Right now, I have a class that extends OncePerRequestFilter added before the UsernamePasswordAuthenticationFilter of the filter chain. In my custom filter, I check to see if the JWT Token is expired in the doFilterInternal method.

If it has expired, I set the status to 403 response.setStatus(HttpStatus.SC_FORBIDDEN); and I write the content

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {

        ...


        if (jwtTokenUtil.isTokenExpired(jwtToken)) {
                response.setStatus(HttpStatus.SC_FORBIDDEN);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write(error.toString());
                response.getWriter().flush();
                response.getWriter().close();
        }

        ...

    }

If I set the status code and do not write the content, there are no exceptions and the user gets the correct status code but the content is automatically created by Spring.

If I set the status code and write the content, then everything actually works from the user's perspective, but internally there is an exception that occurs, talking about how the response was already written to.

I want to do things the correct way; there is probably some class to overwrite and customize so that I can customize the content based on the error code, but I have not been able to find any information on that.

Edit: This is the exception being thrown internally if I try to write to the body

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:123) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:158) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
    at com.abc.web.config.JwtRequestFilter.doFilterInternal(JwtRequestFilter.java:130) ~[classes/:na]
1

1 Answers

0
votes

Instead of setting the response status in the filter, I would recommend you create a custom exception like - TokenExpiredException and throw it in the if block in your code -

if (jwtTokenUtil.isTokenExpired(jwtToken)) {
    // ... some code ... //
    throw new TokenExpiredException("Token Expired");
    // ... some code ... //        
}

Then you can create a @ControllerAdvice which can be the central class to handle all your exceptions. There will be a clear segregation of concerns in the code. The same can be done in the following way -

Your application.properties file should disable the default handlers -

spring.mvc.throw-exception-if-no-handler-found=true

Then you can create a class to handle the exceptions -

@RestControllerAdvice
public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler {
    
    @ResponseBody
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = {TokenExpiredException.class})
    public ApiResponse handleTokenExpiredException(TokenExpiredException ex, WebRequest request) {
        return new ApiResponse(ex.getMessage());
    }
}

Here, ApiResponse is a class that represents your custom response message.