Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring MVC multipart receipt import: validate before storing

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A multipart import endpoint receives a named part, checks its bytes and stores it under a server-generated path.

Download Spring source kit

The checked request boundary

The local controller accepts a part named file at POST /contract/imports. It rejects an empty or missing part, checks a small byte budget, decodes strict UTF-8 and requires a receipt_id,amount_minor header before writing a temporary file. A valid upload returns the generated basename. The test sends ../../outside.csv as the original filename and confirms that the stored file stays under the temporary directory with a new name.

This is a standalone MockMvc controller, not an endpoint in ReceiptApplication. The byte-budget lesson explains the two checks. The path lesson covers why the filename is ignored. The file's declared text/csv type is not enough to accept it; the test submits HTML while claiming CSV and receives 422.

Define an import lifecycle

The fixture only stages bytes. It does not parse all CSV records, insert receipts, scan for malware, choose a durable object store or clean old staged files. A real import should assign an owner, record stage state, set retention, validate each record and publish a result without holding the HTTP request open for unbounded work. The project plan separates these steps.

Checked source

Java
@PostMapping("/contract/imports")
String receive(@RequestParam("file") MultipartFile file) throws IOException {
    if (file.isEmpty())
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
    if (file.getSize() > MAX_BYTES)
        throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE);
    byte[] content;
    try (var input = file.getInputStream()) {
        content = input.readNBytes(MAX_BYTES + 1);
    }
    if (content.length > MAX_BYTES)
        throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE);
    // Strict UTF-8 and header checks run before this file is created.
    Path stored = Files.createTempFile(storage, "receipt-import-", ".csv");
    Files.write(stored, content);
    return stored.getFileName().toString();
}

Verification boundary

MultipartReceiptImportTest.storesValidatedBytesUnderGeneratedName and MultipartReceiptImportTest.rejectsEmptyOrMissingPart in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete test.

Costs and limits

The test uses MockMultipartFile and a JUnit temporary directory. It does not exercise a servlet container's multipart parser, proxy upload limits, disk exhaustion, concurrent uploads or durable retention. Holding the accepted bytes in memory is bounded to 128 bytes in this fixture; a production budget needs a different value and workload measurement.

Common Mistakes

  • Do not use the client filename as a storage path.
  • Do not trust MIME text without checking bytes.
  • Do not call a staged file an imported receipt batch.

Read next

Spring multipart size limits: enforce parser and application budgets, Spring file uploads: discard the supplied filename before writing, Spring CSV uploads: reject malformed UTF-8 and an unknown header, Spring Boot receipt import project: stage, validate and report without hiding failure.

spring
spring-boot
multipart-receipt-import
Storage details