Closed sailingHobbit closed 3 years ago
Thanks for the report.
Spring MVC doesn't set a Content-Length
header for JSON responses as doing so would require it to buffer the response in memory. The fact that your responses have a Content-Length
header tells me that we don't have enough information to help you here. There must be something in your app that's doing that buffering, setting the Content-Length
header, and then writing JSON to the response, but you haven't told us what it is.
It would also be useful to know which container you're using. You've mentioned DispatcherServlet
so it's probably Tomcat but it would be good to know for sure.
If you would like us to spend some more time investigating, please spend some time providing a complete yet minimal sample that reproduces the problem. You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.
Thanks for your feedback! I did not know about Spring MVC not setting a Content-Length
header (and obviously no one in my team did either or I think they would have mentioned that). I will have a look into it and search for the dependency that might actually be causing the issue. This is a really helpful information and would explain why no one else seems to be having that issue with 2.4.8/2.5.2. :-)
Yes you're right, we are using Tomcat.
I will try to build a fully working example but that will require some time since we have a rather complex Maven structure and also some company internal dependencies (one of which might even be the reason for den Content-Length
header).
I will post again as soon as I either have a working sample project with the issue running or found out what is creating the Content-Length
header.
The hint concerning the Content-Length
did the trick and pointed us in the right direction. The root cause seems to be us logging payloads of response by using org.springframework.web.util.ContentCachingResponseWrapper
.
The relevant code is the following (I just removed some internal imports):
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public abstract class AbstractPayloadLoggingFilter extends OncePerRequestFilter {
private final PayloadLoggingCatalog payloadLoggingCatalog = CatalogFactory.create()
.createCatalog(PayloadLoggingCatalog.class, getModuleName());
private PayloadLoglevel payloadLoglevel;
protected abstract String getModuleName();
protected void setPayloadLoglevel(PayloadLoglevel payloadLoglevel) {
this.payloadLoglevel = payloadLoglevel;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest cachedRequest = new RequestWrapper(httpServletRequest);
HttpServletResponse cachedResponse = new ContentCachingResponseWrapper(httpServletResponse);
try {
filterChain.doFilter(cachedRequest, cachedResponse);
} finally {
if (PayloadLoglevel.ALL == payloadLoglevel ||
(PayloadLoglevel.ERROR == payloadLoglevel && isError(httpServletResponse))) {
String correlationId = cachedRequest.getHeader(Constants.HEADER_CORRELATION_ID);
payloadLoggingCatalog.signalRequest(((RequestWrapper) cachedRequest).getBody())
.mergedWithCustomTopLevelAttributes(Attributes.create().with(Constants.KEY_CORRELATION_ID, correlationId))
.log();
payloadLoggingCatalog.signalResponse(getResponseData(cachedResponse))
.mergedWithCustomTopLevelAttributes(Attributes.create().with(Constants.KEY_CORRELATION_ID, correlationId))
.log();
}
((ContentCachingResponseWrapper) cachedResponse).copyBodyToResponse();
}
}
private static boolean isError(HttpServletResponse httpServletResponse) {
HttpStatus status = HttpStatus.valueOf(httpServletResponse.getStatus());
return status.is4xxClientError() || status.is5xxServerError();
}
private static String getResponseData(final HttpServletResponse response) throws IOException {
String payload = "null";
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, StandardCharsets.UTF_8);
wrapper.copyBodyToResponse();
}
}
return payload;
}
}
If I build a context that does not include an Implementation of this PayloadLoggingFilter
the issue does not occur and also responses do not include a Content-Length
header.
Looking into ContentCachingResponseWrapper
I suspect that it is responsible for setting the Content-Length
header.
Since Spring Boot 2.4.8 and 2.5.2 both include Spring Web 5.3.8 while Boot 2.4.5 has Web 5.3.6, there might have been a change between those versions of Spring Web that causes the invalid Content-Length
header.
Therefore this is obviously not a Spring Boot issue but rather Spring Web. Can the issue be moved or should I open a new one within Spring Web?
I'm not sure this is a Spring Framework issue.
Your filter implementation might be involved here, since it's calling copyBodyToResponse
multiple times and async dispatches are not considered.
Maybe by the time it's calling copyBodyToResponse
a first time, the content is written and the Content-Length header is set; some more content might be written later to the cached body, but another call to copyBodyToResponse
will not update the Content-Length since it's already been set. This would explain what you're seeing.
I think the ShallowEtagHeaderFilter
implementation could help here.
Sounds legit that there might be something else that adds "bytes" to the response (although I doubt that we have a Filter in our code that changes the response body itself). Therefore I am with you that it is not an issue within Spring (which would also explain why we could not find anyone else with a similar issue using Google). After discussing it with the team we decided to completely remove the payload logging as we do not really use it anymore. This will fix the problem for us. Thank you all a lot for the helpful input, especially pointing us in the right direction what actually caused the issue! :)
We tried updating our microservices from Spring Boot 2.4.5 first to 2.4.8 and then (as 2.4.8 showed the behavior described below) to 2.5.2 which unfortunately has the same issue.
Observed behavior
When clients call the REST API of our services they get the following error:
org.apache.http.ConnectionClosedException: Premature end of Content-Length delimited message body (expected: 129,072; received: 116,446)
Whileexpected
value varies per received response,received
is usually the same (might be affected by new deployments of the service providing the REST API). It does not apply to all responses but only around 1% and only those with "large" response bodies (100-300 kb). Also it does not start directly after the service has been deployed but usually after a couple of dozen requests have been made.Assumptions
We suspect the issue to be on the server side as a rollback to Spring Boot 2.4.5 solved the problem. Therefore it's unlikely to be a client or infrastructure issue. Also it affects multiple services, some deployed in AWS ECS and some in AWS EKS which makes an infrastrucutre issue also unlikely.
Code & Stack trace
There is no try/catch block involved in building the response (one of the first things google brought up when searching for the exception message). Unfortunately I cannot include the original source code (confidential) but tried to create something which is similar from a technical perspective: Controller:
DTO (Request is just the version attrbiute):
Stacktrace (client side), omitted parts of internal code:
Server logs On the server side we do not see any errors or warnings in the log for these requests. We even enabled debug logging and the last log from the server side is from
org.springframework.web.servlet.DispatcherServlet
with the msgCompleted 200 OK
. The usual log from our own (AOP based) logging framework for a completed request is missing. There is also a log message from an nginx controller (used to route requests from the company network to the AWS EKS cluster) which shows a latency of about 1 minute for the erroneous requests. In normal cases this is aber 50-200 milliseconds. It also shows the response size as seen in the exception message asreceived
. Of course this log line is only present for requests directly from the company network. Requests from within the EKS cluster do not pass through that nginx controller.Any ideas on what might cause the issue? As it affects both 2.4.8 and 2.5.2 we suspect some common dependency which is still in an older version in Boot 2.4.5.
Best regards Ben