Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Streaming upload via @Bean-provided RestTemplateBuilder buffers full file

I'm building a reverse-proxy for uploading large files (multiple gigabytes), and therefore want to use a streaming model that does not buffer entire files. Large buffers would introduce latency and, more importantly, they could result in out-of-memory errors.

My client class contains

@Autowired private RestTemplate restTemplate;

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {

    int REST_TEMPLATE_MODE = 1; // 1=streams, 2=streams, 3=buffers

    return 
        REST_TEMPLATE_MODE == 1 ? new RestTemplate() :
        REST_TEMPLATE_MODE == 2 ? (new RestTemplateBuilder()).build() :
        REST_TEMPLATE_MODE == 3 ? restTemplateBuilder.build() : null;
}

and

public void upload_via_streaming(InputStream inputStream, String originalname) {

    SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    requestFactory.setBufferRequestBody(false);
    restTemplate.setRequestFactory(requestFactory);

    InputStreamResource inputStreamResource = new InputStreamResource(inputStream) {
        @Override public String getFilename() { return originalname; }
        @Override public long contentLength() { return -1; }
    };

    MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
    body.add("myfile", inputStreamResource);

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.MULTIPART_FORM_DATA);

    HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body,headers);

    String response = restTemplate.postForObject(UPLOAD_URL, requestEntity, String.class);
    System.out.println("response: "+response);
}

This is working, but notice my REST_TEMPLATE_MODE value controls whether or not it meets my streaming requirement.

Question: Why does REST_TEMPLATE_MODE == 3 result in full-file buffering?


References:

  • How to forward large files with RestTemplate?
  • How to send Multipart form data with restTemplate Spring-mvc
  • Spring - How to stream large multipart file uploads to database without storing on local file system -- establishing the InputStream
  • How to autowire RestTemplate using annotations
  • Design notes and usage caveats, also: restTemplate does not support streaming downloads
like image 493
Brent Bradburn Avatar asked Jul 20 '18 00:07

Brent Bradburn


People also ask

What is RestTemplateBuilder?

public class RestTemplateBuilder extends Object. Builder that can be used to configure and create a RestTemplate . Provides convenience methods to register converters , error handlers and UriTemplateHandlers .


1 Answers

In short, the instance of RestTemplateBuilder provided as an @Bean by Spring Boot includes an interceptor (filter) associated with actuator/metrics -- and the interceptor interface requires buffering of the request body into a simple byte[].

If you instantiate your own RestTemplateBuilder or RestTemplate from scratch, it won't include this by default.


I seem to be the only person visiting this post, but just in case it helps someone before I get around to posting a complete solution, I've found a big clue:

restTemplate.getInterceptors().forEach(item->System.out.println(item));

displays...

org.SF.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor

If I clear the interceptor list via setInterceptors, it solves the problem. Furthermore, I found that any interceptor, even if it only performs a NOP, will introduce full-file buffering.


public class SimpleClientHttpRequestFactory { ...

I have explicitly set bufferRequestBody = false, but apparently this code is bypassed if interceptors are used. This would have been nice to know earlier...

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

public abstract class InterceptingHttpAccessor extends HttpAccessor { ...

This shows that the InterceptingClientHttpRequestFactory is used if the list of interceptors is not empty.

/**
 * Overridden to expose an {@link InterceptingClientHttpRequestFactory}
 * if necessary.
 * @see #getInterceptors()
 */
@Override
public ClientHttpRequestFactory getRequestFactory() {
    List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
    if (!CollectionUtils.isEmpty(interceptors)) {
        ClientHttpRequestFactory factory = this.interceptingRequestFactory;
        if (factory == null) {
            factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
            this.interceptingRequestFactory = factory;
        }
        return factory;
    }
    else {
        return super.getRequestFactory();
    }
}

class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { ...

The interfaces make it clear that using InterceptingClientHttpRequest requires buffering body to a byte[]. There is not an option to use a streaming interface.

    @Override
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
like image 155
Brent Bradburn Avatar answered Oct 05 '22 09:10

Brent Bradburn