Description:
Recently, we upgraded our project from Spring Boot 2.7.18 to Spring Boot 3.5.0, and after the upgrade, one of our features stopped working properly.
Overview:
In our application, Service A intercepts incoming requests using org.springframework.web.servlet.HandlerInterceptor
, and then forwards the request to Service B using RestTemplate
.
This functionality worked correctly in Spring Boot 2.7.18, but issues have occurred after upgrading to 3.5.0.
Forwarding Logic:
When Service A receives a request:
- All headers from the original request are copied into a MultiValueMap
to be used as headers for the new request.
- The body of the original request is copied into a MultiValueMap<String, Object>
to be used as the new request body.
- An HttpEntity<MultiValueMap<String, Object>>
is created as follows:
HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(form, headers);
- The request is then forwarded using RestTemplate.exchange():
restTemplate.exchange(redirectUrl, HttpMethod.valueOf(method), formEntity, String.class);
When Service A forwards a request with Content-Type: multipart/form-data, Service B returns a 500 error (no issues have been found with other content types).
Below is the error log from Service B:
Caused by: org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException: Processing of multipart/form-data request failed. Stream ended unexpectedly
at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:296) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.Request.parseParts(Request.java:2587) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.Request.getParts(Request.java:2487) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:773) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:94) ~[spring-web-6.1.15.jar:6.1.15]
... 60 more
Caused by: org.apache.tomcat.util.http.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly
at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:959) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:857) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:132) ~[?:?]
at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:132) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:106) ~[?:?]
at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:96) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:292) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.Request.parseParts(Request.java:2587) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.Request.getParts(Request.java:2487) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:773) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:94) ~[spring-web-6.1.15.jar:6.1.15]
... 60 more
We were able to observe some details through packet capture:
The first image shows the request sent from the browser to Service A, which appears to be normal.
The second image shows the request forwarded from Service A to Service B. The body of the request is incomplete and appears to be truncated at the end.
While tracing the request sending process through RestTemplate
, we found the following:
- When handling
multipart/form-data
requests,FormHttpMessageConverter
is used. - In our case, no explicit
Content-Type
was set for each part. - As a result, it falls back to
StringHttpMessageConverter
, which adds a default header (text/plain;charset=UTF-8
) to each part. - This increases the actual body size.
However, since we copied all headers from the original request — including the Content-Length
— it's likely that this original Content-Length
value was used for the forwarded request, even though the body had been modified.
This mismatch between the declared and actual body length leads to the issue where the body appears truncated at the end.
Version Comparison
We compared the implementation of org.springframework.http.client.HttpComponentsClientHttpRequest#executeInternal
between versions:
- In Spring Boot 2.7.18: Used
org.apache.http.entity.ByteArrayEntity
, which recalculates the content length based on the actual body data.
- In Spring Boot 3.5.0: Uses
HttpComponentsClientHttpRequest.BodyEntity
, which does not recalculate the content length and instead uses the value directly from theContent-Length
header.
This change causes the body to be cut off when its actual size differs from what’s specified in the header.
Current Workaround
Our current solution is to filter out the Content-Length
header when copying headers in Service A before forwarding the request. This allows the HTTP client to automatically calculate the correct body length.
Suggested Improvement
We believe that in HttpComponentsClientHttpRequest.BodyEntity
, there should be logic to recalculate the body length if the body has been transformed during message conversion, especially for multipart/form-data requests.
This would prevent mismatches between the declared and actual body sizes, avoiding issues like stream truncation or malformed multipart parsing on the receiving side.
Comment From: bclozel
Thanks for the detailed analysis. I think the current workaround should not be considered as a workaround but rather a best practice. In that sense, this issue is quite similar to #34536 and #21523: if your application is a proxy, it should behave as such and filter/rewrite headers for consistency and security reasons.
Still, I'll check whether we can or should reinstate the former behavior.
Comment From: bclozel
I think the behavior change you are seeing is due to the client factories not buffering the entire request bodies anymore. This is an important efficiency improvement that saves a lot of memory for applications. See https://github.com/spring-projects/spring-framework/issues/30557
This change is also called out in the upgrade notes; this says that you can manually opt-in to use a BufferingClientHttpRequestFactory
if you wish to reinstate the former behavior.
You can of course do this, but I would advise revisiting your proxy implementation in all cases: you should ensure that security headers (like CORS), Forwarded headers, "Content-Length", "Transer-Encoding" headers are not copied over without any processing.
Thanks for this report!