I came across a strange issue while working on troubleshooting an issue. In my Spring Boot application, I have a GET REST end-point which returns a POJO which looks like:
@GetMapping(value = "/dto", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReportDto> dto() {
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add("Controller processed time",LocalDateTime.now().toString());
return new ResponseEntity<ReportDto>(new ReportDto(), headers, HttpStatus.OK);
}
I have an interceptor which looks like:
@Slf4j
@Component
public class AuditInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion
(HttpServletRequest request, HttpServletResponse response, Object
handler, Exception exception) throws Exception {
Thread.sleep(3000);
log.info((LocalDateTime.now())+" : Audit event stored");
}
The Thread.sleep(3000) is to simulate the latency we face from audit event store periodically.
As per design, the audit event is supposed to be written after the response is committed. This is to avoid delay for the client in case of a delay in writing the event.
Surprisingly, the client is receiving the response only after 3 seconds (the added delay), which means the response is committed after the ‘afterCompletion’ method is executed. When the response type is String, the response is committed before afterCompletion is executed. I had tried the response type Integer, boolean and int as well. Except for String, all other types (the types I tried) are getting written only after the afterCompletion is executed.
I have tried different clients. The behavior for each of them are:
- React with axios (both sync and async) – Response body and code are received after the added delay
- Postman – Response code 200 is received immediately, but the body was received after the added delay
- curl – Response printed immediately, but the connection closed only after the added delay
From the behavior observed from curl, it is clear that the delay is not in the Http Message Converter (Jackson by default in Spring Boot).
I have a workaround solution for my issue, which looks like:
@GetMapping(value = "/custom", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> custom() throws JsonProcessingException {
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add("Controller processed time",LocalDateTime.now().toString());
ObjectMapper objectMapper = new ObjectMapper();
return new ResponseEntity<String>(objectMapper.writeValueAsString(new ReportDto()), headers,
HttpStatus.OK);
}
I prefer to have the right solution for my problem. Appreciate if anyone can help me understand the root cause so that we can come up with the right solution.
Please check the demo code at https://github.com/pkurianjohn/InterceptorAfterCompletion.
Thank you.
2
Answers
Upon further analysis, it was found that the getContentLength(Object, ContentType) method in AbstractHttpMessageConverter is unable to find the size of the object, hence unable to set the Content-Length header. As the number of bytes to be written is not available, the response will not be written till the output stream is closed, which happens after the after completion.
To fix the issue, registered a custom Http Message Converter extended from MappingJackson2HttpMessageConverter with the overridden method writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage).
The overridden method snippet is as below:
The full demo code is available at https://github.com/pkurianjohn/InterceptorAfterCompletion/tree/content-length-fix.
I believe the interceptor is called in the request handler, so if you want to take a lot of time, you’re better off doing that in a separate thread or thread pool.
Also, if you want to set response headers, do that in
postHandle
. By the timeafterCompletion
is invoked, the response has already been committed.