Version

Description

  1. Set the custom exception class to throw in the postHandle() of the HandlerInterceptor
  2. Register the HandlerInterceptor class I added
  3. Register and test a simple API

CustomException

public class CustomException extends RuntimeException { }

TestInterceptor

public class TestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("prehandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("posthandle");
        throw new CustomException();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new TestInterceptor()).addPathPatterns("/test");
      registry.addInterceptor(new TestInterceptor2()).addPathPatterns("/test2");
      registry.addInterceptor(new TestInterceptor3()).addPathPatterns("/test3");
      registry.addInterceptor(new TestInterceptor4()).addPathPatterns("/test4");
    }
}

ExceptionHandlers

@RestControllerAdvice
public class ExceptionHandlers {

    @ExceptionHandler(CustomException.class)
    public String handleException(){
        System.out.println("custom exception was occured");
        return "custom exception was occured";
    }

    @ExceptionHandler(CustomException2.class)
    public ServerResponse handleException2(){
        System.out.println("custom exception2 was occured");
        return new ServerResponse(500, "error was occured");
    }

    @ExceptionHandler(CustomException3.class)
    public String handleException3(){
        System.out.println("custom exception3 was occured");
        return "custom exception was occured";
    }

    @ExceptionHandler(CustomException4.class)
    public ServerResponse handleException4(){
        System.out.println("custom exception4 was occured");
        return new ServerResponse(500, "error was occured");
    }


}

TestController

@RestController
public class TestController {

    @GetMapping("/test")
    public ServerResponse test() {
        return new ServerResponse(0, "hello spring");
    }

    @GetMapping("/test2")
    public ServerResponse test2() {
        return new ServerResponse(0, "hello spring");
    }

    @GetMapping("/test3")
    public String test3() {
        return "hello spring";
    }

    @GetMapping("/test4")
    public String test4() {
        return "hello spring";
    }
}

Result

I tested a total of four APIs

API Normal Response Type ExceptionHandler Response Type
/test record String
/test2 record record
/test3 String String
/test4 String record
  • GET /test
{
  "code": 0,
  "description": "hello spring"
}custom exception was occured
  • GET /test2
{
  "code": 0,
  "description": "hello spring"
}{
  "code": 500,
  "description": "error was occured"
}
  • GET /test3
hello spring
  • GET /test4
hello spring

When returning a class type such as a record type, it seems that the normal response and the error response are returned together.

If an exception occurs in afterCompletion(), it does not go to ExceptionHandler because the response to be returned has already been created, but if an exception occurs in postHandle(), it seems to go to ExceptionHandler.

I'm not sure if this response is intended, but even if it is intended, the exception in postHandle() is returned with a normal response and a duplicate response.

Is this an intended or not?

Additional Information

  • I reproduced this issue from Spring Boot 2.5.10, 2.6.10 too.
  • The same symptoms were found in the case of writing in Kotlin
  • If this is a bug, I didn't debug it in detail, but I think I can solve it with some code correction in the part where the Spring actually write the response body
  • In other words, if I have a little help, I think I can raise the PR 🙂

Comment From: rstoyanchev

The 200 status is expected because the @ExceptionHandler does not set it.

For the aggregated content, the behavior depends on the underlying library (Jackson in this case) as to whether it leaves the response stream open. I'm not sure it's worth spending any effort to change anything. It is a bit of an odd scenario in the sense that the controller method has succeeded, the response is written, and there is no way to undo that.

That means, there isn't much that an @ExceptionHandler method could do to "handle" this. Note that if the controller methods had had enough content to write, eventually it would exceed the Servlet container's buffer, the response would be committed and the response status would be not possible to change. So an @ExceptionHandler can't do much reliably anyway, and I'd argue that a postHandle method should not allow any exceptions to escape, especially for an @RestController.