Uploading Files with Spring RestTemplate: MultipartFile and Streaming
Learn to upload files using Spring RestTemplate with MultipartFile and streaming.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17 or later
- ✓Spring Boot 3.x project with spring-web dependency
- ✓Basic understanding of RestTemplate and HTTP multipart requests
- Use
LinkedMultiValueMapto wrapMultipartFilewithByteArrayResourcefor file upload via RestTemplate. - Set
Content-TypetoMediaType.MULTIPART_FORM_DATAexplicitly to avoid encoding issues. - For large files, use
InputStreamResourcefor streaming to prevent OutOfMemoryError. - Handle
RestClientExceptionwith retry logic for transient network failures. - Always log request/response sizes to debug upload failures in production.
Imagine you're mailing a package. RestTemplate is the postal service, MultipartFile is the package with its contents, and streaming is like sending a huge package piece by piece so you don't run out of space in your mailbox. This tutorial shows you how to properly address and send that package without losing parts or jamming the system.
File uploads are a staple of web applications, but doing them right with Spring RestTemplate is surprisingly tricky. I've seen countless production outages caused by naive implementations—like the time a fintech startup's payment processing system crashed because they tried to upload a 500MB CSV file in a single request, blowing up the heap. The official docs show you the happy path, but they don't tell you about the edge cases that will bite you at 3 AM.
In this article, I'll walk you through uploading files using Spring RestTemplate with MultipartFile and streaming. We'll cover the correct way to build multipart requests, how to handle large files without killing your JVM, and the gotchas that the official documentation conveniently omits. You'll learn why ByteArrayResource is your friend, when to use InputStreamResource, and how to debug upload failures in production.
By the end, you'll have a robust file upload strategy that works under load and won't surprise you in production. Let's get our hands dirty.
Setting Up the Multipart Request with RestTemplate
To upload a file using RestTemplate, you need to build a multipart request manually. The trick is to use LinkedMultiValueMap and wrap your file in a ByteArrayResource (or InputStreamResource for streaming). Here's the canonical approach:
```java RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); // For MultipartFile from Spring MVC body.add("file", new ByteArrayResource(multipartFile.getBytes()) { @Override public String getFilename() { return multipartFile.getOriginalFilename(); } });
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers); ResponseEntity<String> response = restTemplate.exchange( "http://example.com/upload", HttpMethod.POST, requestEntity, String.class ); ```
Notice I override getFilename() in ByteArrayResource – this is critical because without it, the server may receive a file named "file" instead of the actual name. The official docs don't mention this, but I've seen production issues where file extensions were lost because of this oversight.
For a standalone file (not from a web request), you can use FileSystemResource:
``java FileSystemResource fileResource = new FileSystemResource(new File("/path/to/file.pdf")); body.add("file", fileResource); ``
But beware: FileSystemResource does not override getFilename() by default in older Spring versions – it uses the filename from the File object, which is usually correct. However, if you need to change the filename, you'll need a custom wrapper.
ByteArrayResource does not implement getFilename() – it returns null. I once spent 2 hours debugging why the server received a file with no extension. The fix is the anonymous override shown above.ByteArrayResource with getFilename() override for small files; use FileSystemResource for files already on disk.Streaming Large Files to Avoid OOM
When dealing with large files (say >100MB), loading the entire file into memory with file.getBytes() is a recipe for disaster. Instead, you should stream the file using InputStreamResource. Here's how:
``java InputStreamResource inputStreamResource = new InputStreamResource( multipartFile.getInputStream()) { @Override public String getFilename() { return multipartFile.getOriginalFilename(); } @Override public long contentLength() { return -1; // unknown length } }; body.add("file", inputStreamResource); ``
Note that we override contentLength() to return -1 because we don't know the length without reading the entire stream (which defeats the purpose). The server must support chunked encoding to handle this. Most modern servers (Tomcat, Netty) do.
But there's a catch: InputStreamResource wraps the input stream, but the underlying HTTP connection will read from it. If the stream is not buffered, you may get poor performance. Always wrap your input stream in a BufferedInputStream:
``java BufferedInputStream bufferedInputStream = new BufferedInputStream(multipartFile.getInputStream()); InputStreamResource resource = new InputStreamResource(bufferedInputStream) { ... }; ``
Also, set a reasonable buffer size on the SimpleClientHttpRequestFactory:
``java SimpleClientHttpRequestFactory factory = new ``SimpleClientHttpRequestFactory(); factory.setBufferSize(4096); // 4KB buffer restTemplate.setRequestFactory(factory);
This prevents the default 8KB buffer from being too small for streaming.
InputStreamResource with BufferedInputStream and set timeouts to prevent OOM and thread exhaustion.Handling MultipartFile from Spring MVC Controllers
In a typical Spring MVC application, you receive a MultipartFile from a controller endpoint. When you need to forward that file to another service via RestTemplate, you have two choices: read the bytes or stream the input stream. For small files (<10MB), reading bytes is simpler. For larger files, use streaming.
Here's a controller that receives a file and forwards it:
``java @PostMapping("/upload") public String handleUpload(@RequestParam("file") MultipartFile file) { return fileUploadService.uploadFile(file, "http://internal-service/upload"); } ``
But there's a subtlety: MultipartFile.getBytes() loads the entire content into memory. If the file is large, this can cause OOM even before the RestTemplate call. Always check file size before calling getBytes():
``java if (file.getSize() > 10 1024 1024) { // use streaming } else { // use bytes } ``
Another gotcha: MultipartFile is not serializable. If you need to pass it across microservices, you must convert it to a byte array or stream before sending. Never try to serialize the MultipartFile object itself.
MultipartFile.getBytes() now throws IllegalStateException if the file has been read already. Always call getBytes() only once or use the input stream.MultipartFile.getInputStream() for streaming to avoid loading entire file into memory.What the Official Docs Won't Tell You
The official Spring documentation covers the basics of multipart requests, but here are the hard-earned lessons from production:
1. Filename handling is broken in ByteArrayResource ByteArrayResource does not implement getFilename() – it returns null. The server will receive a file with no name. Always override getFilename() to provide the original filename. I've seen this cause silent failures where the file is saved without an extension.
2. Content-Type charset can break multipart If you set MediaType.MULTIPART_FORM_DATA with a charset (e.g., MediaType.valueOf("multipart/form-data;charset=UTF-8")), some servers will reject the request. The correct way is to set Content-Type header to MediaType.MULTIPART_FORM_DATA without charset. The charset is handled per part if needed.
3. RestTemplate's default buffer size is 8KB For streaming, this can be too small. Use SimpleClientHttpRequestFactory and set bufferSize to 4096 or higher. I once saw a 10x performance improvement by increasing the buffer from 8KB to 64KB.
4. Spring Boot 3.1 has a bug with InputStreamResource and content length In Spring Boot 3.1, if you set contentLength() to -1, the HttpHeaders may still try to calculate the length, causing an IllegalStateException. The workaround is to set Content-Length header explicitly to -1 or omit it. This was fixed in 3.2.
5. MultipartFile's getBytes() is blocking If you're using reactive stack (WebFlux), getBytes() blocks the thread. Use Part from Spring WebFlux instead. But that's a topic for another article.
Error Handling and Retry Logic
File uploads are prone to transient failures: network timeouts, server overload, etc. You should implement retry logic with exponential backoff. Here's a simple approach using Spring Retry:
``java @Retryable(value = {RestClientException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public String uploadFileWithRetry(MultipartFile file, String uploadUrl) { // same upload logic } ``
But be careful: retrying a file upload that partially succeeded can cause duplicate files on the server. Ensure idempotency by including a unique request ID in the headers. The server can then deduplicate.
- 413 Payload Too Large: Reduce file size or split the file.
- 429 Too Many Requests: Retry after the Retry-After header.
- 5xx: Retry with backoff.
Here's a manual retry example with exponential backoff:
``java int maxRetries = 3; int delay = 1000; for (int i = 0; i < maxRetries; i++) { try { return restTemplate.exchange(...); } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.PAYLOAD_TOO_LARGE) { throw e; // no retry } throw e; } catch (RestClientException e) { if (i == maxRetries - 1) throw e; Thread.sleep(delay); delay *= 2; } } ``
Testing File Uploads with RestTemplate
Unit testing file uploads can be tricky because you need to mock the MultipartFile and the RestTemplate call. Here's a clean approach using Mockito:
```java @Test void testUploadFile() { MultipartFile mockFile = mock(MultipartFile.class); when(mockFile.getOriginalFilename()).thenReturn("test.txt"); when(mockFile.getBytes()).thenReturn("Hello World".getBytes()); when(mockFile.getSize()).thenReturn(11L);
RestTemplate restTemplate = mock(RestTemplate.class); when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) .thenReturn(new ResponseEntity<>("success", HttpStatus.OK));
FileUploadService service = new FileUploadService(restTemplate); String result = service.uploadFile(mockFile, "http://example.com/upload");
assertEquals("success", result); } ```
For integration testing, use a local server with @SpringBootTest and TestRestTemplate. You can also use WireMock to stub the external service:
```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc class FileUploadIntegrationTest {
@Autowired private TestRestTemplate restTemplate;
@Test void testUpload() { // create a temp file File tempFile = File.createTempFile("test", ".txt"); // use TestRestTemplate to upload LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("file", new FileSystemResource(tempFile)); ResponseEntity<String> response = restTemplate.postForEntity("/upload", body, String.class); assertEquals(200, response.getStatusCodeValue()); } } ```
Remember to clean up temp files after tests.
MultipartFile and RestTemplate for unit tests; use TestRestTemplate or WireMock for integration tests.The 2 AM Heap Blowup: When File Uploads Killed a Payment Processor
FileSystemResource directly without streaming.InputStreamResource wrapped in a BufferedInputStream to stream the file directly to the request body without buffering the whole file. Also added a SimpleClientHttpRequestFactory with a buffer size of 4096 bytes.- Never assume default RestTemplate handles streaming for large files—test with realistic file sizes.
- Always monitor heap usage during uploads; use tools like VisualVM or JFR.
- Implement circuit breaker patterns for upload endpoints to prevent cascading failures.
- Set explicit timeouts (connect, read) to avoid thread exhaustion.
- Use
InputStreamResourcefor files >10MB to avoid OOM.
logging.level.org.apache.http=DEBUG.ByteArrayResource with InputStreamResource for streaming. Increase heap if streaming not possible, but prefer streaming.SimpleClientHttpRequestFactory. Default is infinite! Use 30s connect, 60s read.Content-Type is multipart/form-data and no extra charset. Verify file checksum before/after upload.curl -F "file=@test.txt" http://localhost:8080/uploadCheck wire log: logging.level.org.springframework.web=DEBUG| File | Command / Code | Purpose |
|---|---|---|
| FileUploadService.java | public class FileUploadService { | Setting Up the Multipart Request with RestTemplate |
| LargeFileUploadService.java | public class LargeFileUploadService { | Streaming Large Files to Avoid OOM |
| FileUploadController.java | @RestController | Handling MultipartFile from Spring MVC Controllers |
| ByteArrayResourceFix.java | body.add("file", new ByteArrayResource(file.getBytes()) { | What the Official Docs Won't Tell You |
| RetryableUploadService.java | @Service | Error Handling and Retry Logic |
| FileUploadServiceTest.java | @ExtendWith(MockitoExtension.class) | Testing File Uploads with RestTemplate |
Key takeaways
ByteArrayResource with filename override; for large files, use InputStreamResource with streaming to avoid OOM.InputStreamResource.Interview Questions on This Topic
How would you upload a large file using RestTemplate without causing OutOfMemoryError?
InputStreamResource to stream the file content directly from the input stream, avoiding loading the entire file into memory. Wrap the stream in a BufferedInputStream and set a reasonable buffer size on the request factory. Also set connect and read timeouts.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't