File Upload
A profile picture or a PDF invoice has to get from a browser's file picker into your storage somehow. This module is that path, plus the validation gaps that turn an upload endpoint into a security hole.
Learning objectives
- Beginner: Accept a file upload via MultipartFile and save it to disk.
- Intermediate: Validate a file upload's size and content type before trusting it.
- Advanced: Explain why storing uploads directly on the application server's disk doesn't survive a real production deployment, and what replaces it.
◆ The problem
A browser file upload isn't sent as JSON — it's sent as multipart/form-data, an entirely different encoding that bundles the raw file bytes alongside any other form fields in one request body.
Spring's MultipartFile abstracts this away, giving you the uploaded file as a simple object regardless of the multipart-encoding details underneath.
@PostMapping("/upload") public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return ResponseEntity.badRequest().body("Empty file"); } Path path = Paths.get("uploads", file.getOriginalFilename()); Files.write(path, file.getBytes()); return ResponseEntity.ok("Uploaded"); }
💻 Code example
@PostMapping("/upload") public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return ResponseEntity.badRequest().body("Empty file"); } Path path = Paths.get("uploads", file.getOriginalFilename()); Files.write(path, file.getBytes()); return ResponseEntity.ok("Uploaded"); }
⚠ Common real-world trap
Trusting file.getContentType() or the file's extension as proof of what a file actually is has caused real security incidents — a client can rename a .exe to .jpg and set any Content-Type header it wants, since both are entirely client-controlled and easily spoofed.
Real validation checks the file's actual byte signature (magic numbers), not just its claimed metadata, and enforces a maximum size at the framework level:
spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=5MB
| Check | Why |
|---|---|
| Max size (config) | Prevents a single huge upload from exhausting memory/disk. |
| Content-Type header | A hint only — never trust it alone. |
| Magic-number sniffing | Confirms the file's ACTUAL type from its byte content. |
| Filename sanitization | Prevents path traversal via a filename like ../../etc/passwd. |
Saving an uploaded file to the application server's local filesystem works perfectly in a demo and breaks the moment you deploy more than one instance — Instance A saves a file, a later request lands on Instance B (which has no idea that file exists), and the "upload" appears to vanish. Every container restart wipes local disk entirely unless it's on a persistent volume.
Production systems instead upload to object storage (S3, Cloudflare R2, or similar) — every instance talks to the SAME shared storage regardless of which one handled the original upload request, and storage survives deployments and restarts by design.
Want a visual for this concept?
Generate a diagram tailored to “File Upload” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →