Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OutOfMemoryError when uploading large file with RestTemplate and RestClient #32879

Closed
ZIRAKrezovic opened this issue May 23, 2024 · 1 comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: invalid An issue that we don't feel is valid

Comments

@ZIRAKrezovic
Copy link

ZIRAKrezovic commented May 23, 2024

Affects: 6.1.8

Edit: Originally reproduced in 6.1.6, but present in 6.1.8 which is latest version.

When uploading large file using multipart upload using either RestTemplate or RestClient, virtual machine will crash with out of memory error when InterceptingClientHttpRequestFactory wraps the Client's request factory.

  • For RestTemplate, this happens when at least one interceptor is added
  • For RestClient, even plain RestClient.create() results in usage of InterceptingClientHttpRequestFactory

I have narrowed issue to FormHttpMessageConverter::writeMultipart

		if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
			streamingOutputMessage.setBody(outputStream -> {
				writeParts(outputStream, parts, boundary);
				writeEnd(outputStream, boundary);
			});
		}
		else {
			writeParts(outputMessage.getBody(), parts, boundary);
			writeEnd(outputMessage.getBody(), boundary);
		}

When running the code with debugger, and no interceptor exists in RestTemplate, the execution is as follows

without interceptor

As soon as one interceptor is added, the execution is as follows

with interceptor

The InterceptingClientHttpRequest wrapped by InterceptingClientHttpRequestFactory does not implement StreamingHttpOutputMessage, so the second path is used - which in turn ends up using FastByteArrayOutputStream.

However, the InterceptingClientHttpRequest does seem to have code to handle StreamingHttpOutputMessage itself.

The resulting error is:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at org.springframework.util.FastByteArrayOutputStream.addBuffer(FastByteArrayOutputStream.java:325)
	at org.springframework.util.FastByteArrayOutputStream.write(FastByteArrayOutputStream.java:126)
	at org.springframework.http.converter.FormHttpMessageConverter$MultipartOutputStream.write(FormHttpMessageConverter.java:683)
	at java.base/java.io.InputStream.transferTo(InputStream.java:797)
	at java.base/sun.nio.ch.ChannelInputStream.transferTo(ChannelInputStream.java:268)
	at org.springframework.http.converter.ResourceHttpMessageConverter.writeContent(ResourceHttpMessageConverter.java:151)
	at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:140)
	at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:46)
	at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:235)
	at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:549)

I can circumvent this issue for RestTemplate by clearing any interceptors. However, we utilize spring cloud load balancer, which is written as a Request Interceptor, which makes RestTemplate's requestFactory wrapped as InterceptingClientHttpRequestFactory.

I am not sure how to do this for rest client.

I have prepared a reproducer, by generating a file of ~360 MB. It is important to run the test with small heap size, i.e. -Xmx256m is enough. I have updated maven-surefire-plugin to set this - so as long as you use "./mvnw clean verify" the issue will be obvious. My IntelliJ IDEA test configuration is as follows

Intellij run configuration

Examine the MultipartReproducerApplicationTests and comments on how to reproduce the wrong and right scenarios.

https://github.com/ZIRAKrezovic/spring-multipart-reproducer

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label May 23, 2024
@snicoll snicoll added the in: web Issues in web modules (web, webmvc, webflux, websocket) label May 23, 2024
@bclozel
Copy link
Member

bclozel commented May 23, 2024

I think this is the expected behavior.
When using interceptors with RestTemplate or RestClient, the contract requires the entire request body to be provided as a byte array. This means the entire body must be held in memory at once. This explains why message converters cannot leverage the streaming use case.

If you wish to intercept requests in a streaming fashion, you should use WebClient with its ExchangeFilterFunction contract. If you don't need interceptors, removing those is also a possibility.

I can circumvent this issue for RestTemplate by clearing any interceptors. However, we utilize spring cloud load balancer, which is written as a Request Interceptor, which makes RestTemplate's requestFactory wrapped as InterceptingClientHttpRequestFactory.

I cannot speak for the spring cloud load balancer, you should reach out to the team about this use case.

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale May 23, 2024
@bclozel bclozel added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

4 participants