resttemplate multipart post with InputStreamResource not working [SPR-13571]

See original GitHub issue

Greg Adams opened SPR-13571 and commented

I’ve been trying to send a multipart post via restTemplate and have been unable to get it to work with anything but FileSystemResource. In my use case (a weird file-forwarding use case) this forces me to copy a MultiPartFile InputStream into a temp file in order be able to create a FileSystemResource, which seems undesirable.

Here’s a testing version of the file-receiving controller (from another project, running in another servlet container):

@RestController
public class FileReceiveController {
	
	private Log log = LogFactory.getLog(FileReceiveController.class);

	@RequestMapping(method = RequestMethod.POST)
	public void uploadFile(@RequestParam("customerId") int customerId, @RequestPart("file") MultipartFile multipartFile) {
		log.info("customerId: " + customerId);
		log.info("Received multipart file - original filename: " + multipartFile.getOriginalFilename());
		log.info("content-type: " + multipartFile.getContentType());
		log.info("size: " + multipartFile.getSize());
	}
}

Here’s the file-forwarding controller:

@RestController
public class FileForwardController {
	
	private RestTemplate restTemplate;
	
	public FileForwardController() {
		SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
		requestFactory.setBufferRequestBody(false);
		this.restTemplate = new RestTemplate(requestFactory);
	}

	@RequestMapping(method = RequestMethod.POST)
	public void uploadFile(@RequestParam("customerId") int customerId, @RequestPart("file") MultipartFile multipartFile) {
		MultiValueMap<String,Object> parts = new LinkedMultiValueMap<>();
		parts.add("customerId", customerId);
		try {
			// copy to temp file and use FileSystemResource
//			File tempFile = File.createTempFile("xyz", "");
//			FileCopyUtils.copy(multipartFile.getInputStream(), new FileOutputStream(tempFile));
//			parts.add("file", new FileSystemResource(tempFile));
			// OR use InputStreamResource (broken)
			parts.add("file", new InputStreamResource(multipartFile.getInputStream()));
			// OR use ByteArrayResource (broken)
//			parts.add("file", new ByteArrayResource(multipartFile.getBytes()));
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.MULTIPART_FORM_DATA);
		HttpEntity<MultiValueMap<String,Object>> request = new HttpEntity<>(parts, headers);
		restTemplate.exchange("http://localhost:8080", HttpMethod.POST, request, Void.class);
	}
}

In this form, the restTemplate.exchange call throws

org.springframework.web.client.HttpClientErrorException: 400 Bad Request
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
	at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:614)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:570)

The ByteArrayResource form does the same thing. Only the FileSystemResource form works.


Affects: 4.1.7

Issue Links:

  • #18023 Read large data using InputStreamResource at ResourceHttpMessageConverter
  • #16633 RestTemplate with InputStreamResource does not work if Content-Length is not set
  • #19776 HTTP Response should not contain both Transfer-Encoding and Content-Length headers
  • #20990 Consistent treatment of InputStreamResource subclasses
  • #21348 Support use of MultipartFile as input to RestTemplate or WebClient

Referenced from: commits https://github.com/spring-projects/spring-framework/commit/27c12809493954a7521819cc623fc7873ecf2f4f

0 votes, 6 watchers

Issue Analytics

  • State:closed
  • Created 8 years ago
  • Comments:6

github_iconTop GitHub Comments

7reactions
spring-projects-issuescommented, Jan 11, 2019

Marcus Schulte commented

Actually ResourceHttpMessageConverter seems a bit broken to me, when it checks whether it should ask a resource for its content-length by comparing its type to a constant:

@Override
protected Long getContentLength(Resource resource, MediaType contentType) throws IOException {
     // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
     // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
     return (InputStreamResource.class == resource.getClass() ? null : resource.contentLength());
}

This gives anyone inheriting from InputStreamResource a hard time conforming to the Liskov principle.

For me, a combination of a custom-named resource and a slightly altered version of ResourceHttpMessageConverter does the trick:

/**
 * This class leaves it to the resource-implementation to decide whether it can reasonably supply a
 * content-length. It does so by assuming that returning {@code null} or a negative number indicates
 * its unwillingness to provide a content-length.
 *
 */

public class ResourceHttpMessageConverterHandlingInputStreams extends ResourceHttpMessageConverter {

    @Override
    protected Long getContentLength(Resource resource, MediaType contentType) throws IOException {
        Long contentLength = super.getContentLength(resource, contentType);

        return contentLength == null || contentLength < 0 ? null : contentLength;
    }
}

Then, a resource like the following works

 /**
     * Works with {@link ResourceHttpMessageConverterHandlingInputStreams} to forward input stream from
     * file-uploads without reading everything into memory.
     *
     */
    private class MultipartInputStreamFileResource extends InputStreamResource {

        private final String filename;

        public MultipartInputStreamFileResource(InputStream inputStream, String filename) {
            super(inputStream);
            this.filename = filename;
        }
        @Override
        public String getFilename() {
            return this.filename;
        }

        @Override
        public long contentLength() throws IOException {
            return -1; // we do not want to generally read the whole stream into memory ...
        }
    }

Of course, the broken Converter needs to be replaced in the RestTemplate:

List<HttpMessageConverter<?>> messageConverters = this.restTemplate.getMessageConverters();
       for (int i = 0; i < messageConverters.size(); i++) {
           HttpMessageConverter<?> messageConverter = messageConverters.get(i);
           if ( messageConverter.getClass().equals(ResourceHttpMessageConverter.class) )
               messageConverters.set(i, new ResourceHttpMessageConverterHandlingInputStreams());
       }

A method like Resource.isContentLengthAvailable() along with corresponding code to support it would be nice to have …

1reaction
spring-projects-issuescommented, Jan 11, 2019

Brian Clozel commented

In order to properly write the multipart request, the FormHttpMessageConverter configured automatically with the RestTemplate will write all parts; if a part inherits from Resource, it calls the Resource.getFilename() method to get a file name, see the getFilename() method. If no file name is found, then this part is written as a “regular” part, not a file, in the content-disposition part of the message.

In your case, you could do the following:

@RequestMapping(method = RequestMethod.POST)
public void uploadFile(@RequestParam("customerId") int customerId, @RequestPart("file") MultipartFile multipartFile) {
  MultiValueMap<String,Object> parts = new LinkedMultiValueMap<>();
  parts.add("customerId", customerId);
  try {
    parts.add("file", new MultipartFileResource(multipartFile.getInputStream()));
  } catch (IOException e) {
    throw new RuntimeException(e);
  }
  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(MediaType.MULTIPART_FORM_DATA);
  HttpEntity<MultiValueMap<String,Object>> request = new HttpEntity<>(parts, headers);
  restTemplate.exchange("http://localhost:8080", HttpMethod.POST, request, Void.class);
}

private class MultipartFileResource extends InputStreamResource {

  public MultipartFileResource(InputStream inputStream, String filename) {
    super(inputStream);
    this.filename = filename;
  }
  @Override
  public String getFilename() {
    return this.filename;
  }
}

There are a few ways to improve this situation in the framework.

  1. try to “guess” the filename for all parts extending Resource; the problem is, we don’t have that information and we can only try to guess. This can also lead to new issues, since we’d be considering parts as file whereas those weren’t in the past
  2. provide something that looks like a MultipartFileResource implementation and add it in the reference documentation

Could you try the workaround I told you about and confirm this works? Let me know what you think about the solutions listed here.

Thanks!

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to send Multipart form data with restTemplate Spring-mvc
You can proxy a file upload in a spring mvc controller using a InputStreamResource : @RequestMapping(value = "/upload", method = RequestMethod.
Read more >
Uploading MultipartFile with Spring RestTemplate - Baeldung
This quick tutorial focuses on how to upload a multipart file using Spring's RestTemplate. We'll see both a single file and multiple files ......
Read more >
Spring后台RestTemplate post MultipartFile with HttpHeader两 ...
原文:resttemplate multipart post with InputStreamResource not working SPR-13571 代码:client...
Read more >
spring resttemplate multipart/form-data - You.com
When sending objects via RestTemplate, in most cases you want to send POJOs. You can fix this by adding the bytes of the...
Read more >
Uploading a file with a filename with Spring RestTemplate
File upload is sent via Multipart Form Data. ... The receiving server will most likely not see the filename in a different section....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found