Search code examples
javaspringspring-bootresttemplatehttpentity

http call with Multipart form data is failing after spring upgrade


We upgraded spring boot from 3.1.11 to 3.3.3. Post upgrade we are seeing issues with Rest template when we are trying to post a pdf file.

The API call fails abruptly with 400 BAD_REQUEST and the detail says Stream ended unexpectedly

http call has following headers

  1. contentType : multipart/form-data
  2. contentLength :

Http Entity is built as below

  headers.set(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length));

  MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
        ByteArrayResource byteArrayResource = new ByteArrayResource(bytes,
                "Pdf name") {
            @Override
            public String getFilename() {
                return filename;
            }
        };
        parts.add("data", byteArrayResource);
        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(parts, headers);

And exchange is done with help of rest template as below

   exchange(uri, "POST", requestEntity, String.class);

The same piece of code works well with spring boot 3.1.11. So the server side issue is eliminated.

exception that we see is

at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:103) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:942) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:891) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:790) ~[spring-web-6.1.12.jar:6.1.12]
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:672) ~[spring-web-6.1.12.jar:6.1.12]

Our rest template instance is built the following way:

    SSLContext sslContext = sslContextBuilder.build();
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("****", "****");
    keyManagerFactory.init(clientStore, *****);
    sslContext.init(keyManagerFactory.getKeyManagers(), null, ****);


    SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);

    HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create().setSSLSocketFactory(socketFactory).build();

    CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();

    RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));

sslContextBuilder is excluded for brevity and masked a few things for security reasons. But it is configured with tls protocol and necessary certificates.


Solution

  • It seems like Spring has been playing around lately related to how it calculates content length.

    I encountered a similar issue with content length in version 3.2.5.

    Although the content-length header appeared to have the same value whether it was set explicitly or not, it looked like the server expected more data, and the client abruptly closed the connection.

    In 3.3.0 When we let spring calculate the content-length header, I noticed it sent an extra header Transfer-Encoding: chunked and don't see content-length header honored by the server at all. This hints that server is instructed/assumes to read data so long it is available and not restricted by a value sent in header.

    However in 3.1.11 I don't see Transfer-Encoding: chunked being set. Instead, Rest template overrides the content length set by the user to some other value(bytes+metadata) which happens to be the number of bytes server is expected to read.

    More on content-length vs Transfer-Encoding: chunked is explained here

    For now, removing the line that explicitly sets the content-length header resolved the issue.

    headers.set(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length));
    

    Until spring decides to change this in the future xD