Skip to content
Spring Boot sb web 3 min read

File Upload & Download

Spring Boot has built-in multipart support, so accepting file uploads and serving downloads takes very little code. This page covers receiving files with MultipartFile, configuring multipart limits, saving to disk safely, and streaming downloads with ResponseEntity<Resource>.

Receiving a single file

A handler that consumes multipart/form-data binds an uploaded part to a MultipartFile.

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/files")
public class FileUploadController {

    private final StorageService storage;

    public FileUploadController(StorageService storage) {
        this.storage = storage;
    }

    @PostMapping("/upload")
    public UploadResponse upload(@RequestParam("file") MultipartFile file) {
        String stored = storage.store(file);
        return new UploadResponse(stored, file.getSize(), file.getContentType());
    }
}

public record UploadResponse(String filename, long size, String contentType) {}

Request:

curl -X POST http://localhost:8080/api/files/upload \
  -F "[email protected]"

Output:

{ "filename": "report.pdf", "size": 18342, "contentType": "application/pdf" }

The MultipartFile API exposes getOriginalFilename(), getContentType(), getSize(), isEmpty(), getBytes(), getInputStream(), and transferTo(Path).

Multiple files and mixed form data

Accept several files, or files alongside regular fields.

@PostMapping("/upload-many")
public List<String> uploadMany(@RequestParam("files") MultipartFile[] files) {
    return Arrays.stream(files).map(storage::store).toList();
}

@PostMapping("/avatar")
public UploadResponse avatar(@RequestParam String userId,
                             @RequestPart("file") MultipartFile file) {
    return new UploadResponse(storage.store(file), file.getSize(), file.getContentType());
}

Tip: Use @RequestParam for simple file + text fields. Use @RequestPart when a part is itself structured (e.g. a JSON metadata part deserialized by Jackson).

Multipart configuration

Spring Boot enables multipart by default. Tune limits in application.properties:

spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=25MB
spring.servlet.multipart.file-size-threshold=2KB
PropertyMeaning
max-file-sizeLargest single file (default 1MB)
max-request-sizeLargest total request (default 10MB)
file-size-thresholdSize above which data is written to disk

Exceeding a limit raises MaxUploadSizeExceededException, which you should handle in a Controller Advice.

@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
public ProblemDetail tooLarge(MaxUploadSizeExceededException ex) {
    return ProblemDetail.forStatusAndDetail(
            HttpStatus.PAYLOAD_TOO_LARGE, "File exceeds the maximum allowed size");
}

Saving files safely

Never trust the client-supplied filename — it can contain path-traversal sequences. Normalize and confine writes to a base directory.

import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.*;
import java.util.UUID;

@Service
public class StorageService {

    private final Path root = Paths.get("uploads");

    public StorageService() throws Exception {
        Files.createDirectories(root);
    }

    public String store(MultipartFile file) {
        if (file.isEmpty()) throw new IllegalArgumentException("Empty file");

        String original = StringUtils.cleanPath(file.getOriginalFilename());
        String stored = UUID.randomUUID() + "-" + original;
        Path target = root.resolve(stored).normalize().toAbsolutePath();

        if (!target.startsWith(root.toAbsolutePath())) {
            throw new IllegalArgumentException("Path traversal attempt");
        }
        try {
            file.transferTo(target);
        } catch (Exception e) {
            throw new RuntimeException("Failed to store file", e);
        }
        return stored;
    }
}

Warning: A filename like ../../etc/passwd will escape your upload folder if you resolve it naively. Always normalize() and verify the result still startsWith the base directory.

Streaming downloads with ResponseEntity

Return a Resource to stream file contents without loading them all into memory. Set Content-Disposition so the browser downloads (or inlines) the file.

import org.springframework.core.io.*;
import org.springframework.http.*;

@GetMapping("/download/{name}")
public ResponseEntity<Resource> download(@PathVariable String name) throws Exception {
    Path file = Paths.get("uploads").resolve(name).normalize();
    Resource resource = new UrlResource(file.toUri());

    if (!resource.exists() || !resource.isReadable()) {
        return ResponseEntity.notFound().build();
    }

    String contentType = Files.probeContentType(file);
    if (contentType == null) contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;

    return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + resource.getFilename() + "\"")
            .body(resource);
}

Request:

curl -OJ http://localhost:8080/api/files/download/report.pdf

Output:

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
Content-Length: 18342

Use inline instead of attachment in Content-Disposition to display in the browser (e.g. a PDF preview).

Pitfalls

  • Loading large files via getBytes() holds the whole file in memory — prefer transferTo and streaming Resource responses.
  • The default max-file-size of 1MB silently rejects bigger uploads with a 500 unless you raise it and handle the exception.
  • Validate content type and size on the server; the client-provided Content-Type is advisory and spoofable.
Last updated June 13, 2026
Was this helpful?