61

I have a Spring MVC method which returns a ResponseEntity. Depending on the specific data retrieved, it sometimes needs to return a stream of data to the user. Other times it will return something other than a stream, and sometimes a redirect. I most definitely want this to be a stream and not a byte array since it can be large.

Currently, I return the stream using the following snippet of code:

HttpHeaders httpHeaders = createHttpHeaders();
IOUtils.copy(inputStream, httpServletResponse.getOutputStream());

return new ResponseEntity(httpHeaders, HttpStatus.OK);

Unfortunately, this does not allow the Spring HttpHeaders data to actually populate the HTTP Headers in the response. This makes sense since my code writes to the OutputStream before Spring receives the ResponseEntity.

It would be very nice to somehow return a ResponseEntity with an InputStream an let Spring handle it. It also would parallel the other paths of my function where I can successfully return a ResponseEntity. Is there anyway I can accomplish this with Spring?


Also, I did try returning the InputStream in the ResponseEntity just to see if Spring would accept it.

return new ResponseEntity(inputStream, httpHeaders, HttpStatus.OK);

But it throws this exception:

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

I can get my function to work by setting everything on the HttpServletResponse directly, but I would like to do this only with Spring.

David V
  • 10,931
  • 5
  • 39
  • 64
  • I suspect that Spring doesn't know how to convert the InputStream body to the response as AFAIK, there isn't a HttpMessageConverter that by default will work with InputStreams. Perhaps try registering a custom HttpMessageConverter that works with InputStream. ByteArrayHttpMessageConverter should give you most of what you need. – Rob Blake Dec 02 '13 at 17:08
  • Did you try @ResponseBody annotation? – Raknel Dec 02 '13 at 17:13
  • I do have the @ResponseBody annotation on my MVC method. – David V Dec 02 '13 at 18:08

1 Answers1

123

Spring's InputStreamResource works well. You need to set the Content-Length manually, or it appears that Spring attempts to read the stream to obtain the Content-Length.

InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
httpHeaders.setContentLength(contentLengthOfStream);
return new ResponseEntity(inputStreamResource, httpHeaders, HttpStatus.OK);

I never found any web pages suggesting using this class. I only guessed it because I noticed there were a few suggestions for using ByteArrayResource.

David V
  • 10,931
  • 5
  • 39
  • 64
  • 12
    Correct, by default there is also a `ResourceHttpMessageConverter` registered which knows how to handle `org.springframework.core.io.Resource` instances. So wrapping the `InputStream` in an `InputStreamResouce` and returning it will work. – M. Deinum Dec 02 '13 at 19:20
  • 1
    And I assume that this also streams the content, rather than reading it all into memory, so that large files can be given to the client without loading it into memory. Is this correct? – David V Dec 02 '13 at 19:50
  • 2
    Would be pretty useless if it didn't :). But yes it streams (in chunks) the content. You might want to check the source of `ResourceHttpMessageConverter` to see how it is done. – M. Deinum Dec 03 '13 at 08:06
  • 1
    See also http://stackoverflow.com/questions/4693968/is-there-an-existing-fileinputstream-delete-on-close – ThomasRS Sep 05 '15 at 15:32
  • 17
    I came across this, and using this method, I can stream a 500MB file with a 64MB heap -- it's definitely buffering the output and not just allocating a byte array. Many thanks to David V for his answer. The only question remaining is: Does Spring properly close the InputStream any any other resources created? I've been trying to figure this out with Google searches, and I'm not able to locate any information one way or the other. I would *think* that the Spring authors would take steps to avoid resource leaks from code like this, but I would like to know for sure. – elyograg Jun 01 '16 at 16:06
  • 8
    It does close the underlying stream. https://github.com/spring-projects/spring-framework/blob/v4.3.9.RELEASE/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java#L117 – David V Jun 12 '17 at 22:26
  • 1
    I came across this situation and the solution is not working in my case. Error says 'InputStream has already been read' . In the answer code snippet, I suppose "contentLengthOfStream" still needs to be calculated from the InputStream, correct? – Nilesh Dec 15 '17 at 14:43
  • 6
    @Nilesh, you do need your own process to determine `contentLengthOfStream`. Otherwise Spring will open the stream and read it. That won't work well for large streams or streams that you can only read once. – David V Feb 16 '18 at 21:46
  • 2
    David, how did you solve calculating the content length up ahead? I want to stream directly from my repository with a custom @Query in there. I suppose I could do a count query first but that might go out of sync. – Sebastiaan van den Broek May 14 '18 at 10:08
  • 4
    @SebastiaanvandenBroek, that will depend on how you get your stream. We normally read from S3, so we set the `contentLengthOfStream` to the value of Content-Length which is provided by the S3 SDK. You could also do a similar thing if loading from a File by using the file length. – David V May 15 '18 at 18:08
  • 3
    I don't think the `Content-Length` remark is still relevant, it seems [the problem has been fixed in Spring 4.1](https://github.com/spring-projects/spring-framework/issues/16633) with [this change](https://github.com/spring-projects/spring-framework/commit/f0bcb773f9117195ef39acc3b4077c9f41d8afa0). AFAICT the content is not loaded in memory either when you don't specify it. I checked with a heap dump performed during a download, using Eclipse MAT. I am streaming a `Clob` so I don't have immediate access to its size in bytes without reading it. – Didier L Sep 09 '19 at 15:48
  • 1
    I forgot to mention that I had to call `ShallowEtagHeaderFilter.disableContentCaching(request)`, otherwise this filter will still copy the whole stream in memory to compute the ETag. See also [Disable ShallowEtagHeaderFilter when ETag handled in application](https://github.com/spring-projects/spring-framework/issues/22797) – Didier L Sep 09 '19 at 16:24