Version
- Spring Boot
2.7.5
with Spring Boot Starter Web only. - Repository that reproduces symptoms
Description
- Set the custom exception class to throw in the
postHandle()
of theHandlerInterceptor
- Register the
HandlerInterceptor
class I added - 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
.