Spring Boot File Upload
Profile photos, invoices, resumes, and reports all travel as files. A Spring Boot application receives uploaded files through multipart requests, stores them safely, and sends them back on demand. Handling files correctly protects both the server and the users.
Spring MVC wraps every uploaded file in a MultipartFile object. That object exposes the file name, size, type, and bytes through simple methods.
The Parcel Counter Idea
A post office counter accepts parcels. The clerk checks the weight limit, records the sender, places the parcel in a labeled bin, and hands out a receipt. A file upload endpoint follows the same routine: check the file, rename it, store it, and return a reference.
User picks file
|
Browser sends multipart request
|
Controller receives MultipartFile
|
Service checks size and type
|
Service saves file under a safe name
|
Response returns file name or link
Setting Upload Limits
Spring Boot limits uploads by default. Raise or lower the limits in application.properties.
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=10MB
app.upload.dir=uploadsThe first size limit applies to a single file, and the second applies to the whole request. A request above either limit fails with an error before the controller runs.
The Upload Endpoint
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileStorageService storage;
public FileController(FileStorageService storage) {
this.storage = storage;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) {
String savedName = storage.save(file);
return ResponseEntity.status(HttpStatus.CREATED).body(savedName);
}
}The request parameter name, file, must match the form field name that the client sends.
The Storage Service
@Service
public class FileStorageService {
private final Path root;
public FileStorageService(@Value("${app.upload.dir}") String dir) throws IOException {
this.root = Paths.get(dir).toAbsolutePath().normalize();
Files.createDirectories(root);
}
public String save(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("File is empty");
}
String type = file.getContentType();
if (type == null || !(type.equals("image/png") || type.equals("image/jpeg")
|| type.equals("application/pdf"))) {
throw new IllegalArgumentException("File type not allowed");
}
String original = StringUtils.cleanPath(file.getOriginalFilename());
String extension = original.contains(".")
? original.substring(original.lastIndexOf('.')) : "";
String savedName = UUID.randomUUID() + extension;
try {
Files.copy(file.getInputStream(), root.resolve(savedName),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Could not store file", e);
}
return savedName;
}
}A random UUID name prevents two users from overwriting each other's files and hides the original name from path tricks.
The Download Endpoint
@GetMapping("/{name}")
public ResponseEntity<Resource> download(@PathVariable String name) throws IOException {
Resource resource = storage.load(name);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
public Resource load(String name) throws IOException {
Path file = root.resolve(name).normalize();
if (!file.startsWith(root) || !Files.exists(file)) {
throw new FileNotFoundException(name);
}
return new UrlResource(file.toUri());
}The startsWith check blocks path traversal. A malicious name such as ../../secret would otherwise point outside the upload folder.
Testing with curl
curl -X POST http://localhost:8080/api/files \
-F "file=@photo.png"
curl -O http://localhost:8080/api/files/3f1c9c8e-photo.pngSafety Checklist
| Risk | Defense |
|---|---|
| Oversized files | Set max-file-size and max-request-size |
| Dangerous file types | Allow only approved content types and extensions |
| Path traversal | Normalize paths and confirm they stay inside the root folder |
| Name collisions | Generate random file names |
| Disk filling | Set quotas and clean old files |
| Harmful content | Scan uploads with antivirus software for public services |
Choosing Where to Store Files
- Local disk suits small tools and learning projects.
- Cloud object storage such as Amazon S3 suits production systems that run several servers.
- A database blob column suits small files that must stay with their records.
Store only the file name or key in the database, and keep the bytes in storage built for files.
Handling Upload Errors
Uploads fail for many reasons: files above the size limit, wrong file types, full disks, and broken connections. Each reason deserves a clear reply. Spring throws MaxUploadSizeExceededException when a request passes the limit, and a controller advice class catches it and returns status 413 with a friendly message.
@RestControllerAdvice
public class UploadExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<String> tooLarge() {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body("File exceeds the 5 MB limit");
}
}Invalid file types return status 400, and missing files return 404 on download. Users who see precise messages fix problems on their own, and support teams receive fewer tickets. Log every failure with the file name and user id so the team can trace repeated problems.
Best Practices
- Validate every upload on the server, because client checks are easy to bypass.
- Check the real content type and not only the extension.
- Store files outside the web root and serve them through controlled endpoints.
- Require login for private files.
- Return clear errors for empty, oversized, or rejected files.
Safe file handling builds user trust. A short checklist of limits, type checks, and random names removes the most common upload risks.
