Start HereOrdered learning track

Multipart Upload in Java Services

Learn Java Microservices File Handling, State, Configuration and Secret Management - Part 012

Implementasi multipart upload production-grade di Java services: Spring MVC MultipartFile, WebFlux FilePart, buffer boundary, proxy limit, temporary storage, streaming, direct-to-storage, dan backpressure.

12 min read2294 words
PrevNext
Lesson 1270 lesson track01–13 Start Here
#java#microservices#multipart#spring-mvc+5 more

Part 012 — Multipart Upload in Java Services

Multipart upload is not a storage strategy.

It is an HTTP envelope that can accidentally become your memory, disk, proxy, and security problem.

Di part sebelumnya kita mendesain upload/download service sebagai boundary. Sekarang kita masuk implementasi multipart upload di Java service.

Fokus part ini:

  • bagaimana Spring MVC MultipartFile bekerja secara mental model;
  • bagaimana WebFlux FilePart berbeda;
  • apa risiko memory/disk buffering;
  • bagaimana proxy dan application limit harus diselaraskan;
  • kapan upload harus lewat service dan kapan direct-to-storage;
  • bagaimana menghitung checksum sambil streaming;
  • bagaimana menghindari path traversal dan filename trust;
  • bagaimana membuat multipart upload tetap idempotent, observable, dan recoverable.

Multipart sering terlihat sederhana karena framework menyembunyikan detail parsing. Justru itu bahayanya. Saat ukuran file besar, client lambat, proxy timeout, temporary disk penuh, atau content malicious, detail yang disembunyikan framework menjadi failure domain.


1. Mental Model: Multipart Is an Envelope

Request multipart kira-kira seperti ini:

POST /v1/files HTTP/1.1
Content-Type: multipart/form-data; boundary=----abc

------abc
Content-Disposition: form-data; name="ownerId"

CASE-123
------abc
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf

<binary bytes>
------abc--

Yang perlu dipahami:

  • multipart bisa berisi field text dan file part;
  • setiap part punya header sendiri;
  • filename dan Content-Type berasal dari client;
  • parser framework harus membaca request body;
  • file content bisa dibuffer di memory, ditulis ke disk sementara, atau diproses sebagai stream tergantung stack/config;
  • limit bisa terjadi di banyak layer: browser/client, CDN, load balancer, reverse proxy, servlet container, framework, application policy, storage.

Core invariant:

Multipart parser may parse the envelope, but domain service must decide trust.

2. Upload Limit Layers

Jangan hanya set limit di satu tempat.

Setiap layer punya risiko:

LayerLimit yang Relevan
CDN/edgemax body, timeout, WAF inspection
Load balanceridle timeout, request body limit
Reverse proxy/gatewaymax body size, buffering, temp disk
Servlet containermax file size, max request size, file threshold
Spring Bootmultipart properties
Domain policyper purpose/tenant/user size limit
Storageobject size, multipart rules, throughput

Rule:

Outer layer may reject obviously invalid traffic early,
but domain layer remains the final source of business policy.

3. Spring MVC MultipartFile

Spring MultipartFile adalah representasi file yang diterima dalam multipart request. Dokumentasi Spring menjelaskan bahwa isi file dapat disimpan di memory atau sementara di disk, dan user bertanggung jawab memindahkan file ke penyimpanan permanen jika diperlukan.

Typical controller:

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<StoredFileResponse> upload(
    @RequestPart("file") MultipartFile file,
    @RequestPart("ownerId") String ownerId
) {
    // do not put storage logic here
    return ResponseEntity.ok(service.handle(file, ownerId));
}

3.1 Important MultipartFile Methods

MethodUseWarning
getOriginalFilename()display/audit metadatauntrusted; may contain path-like value
getContentType()declared MIMEuntrusted; can be spoofed
getSize()parsed sizevalidate against policy
isEmpty()quick checkempty file may still be valid in some domains? decide policy
getInputStream()streaming readcaller must close stream
transferTo(...)move/copy to destinationdestination must be controlled and safe
getBytes()full content as byte[]dangerous for large files

Avoid:

byte[] bytes = file.getBytes(); // loads whole file into memory

Better:

try (InputStream in = file.getInputStream()) {
    storage.write(in);
}

4. Spring Boot Multipart Configuration

Spring Boot exposes multipart configuration such as max file size and max request size through multipart properties. These are framework/container limits, not business policy.

Example:

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 100MB
      max-request-size: 110MB
      file-size-threshold: 2MB
      location: /var/app/upload-tmp

Interpretation:

PropertyMeaning
max-file-sizemaximum size for an individual uploaded file
max-request-sizemaximum multipart request size, including fields and all files
file-size-thresholdthreshold after which file may be written to disk instead of memory
locationtemporary directory for multipart files

Production advice:

  • set explicit limits;
  • use dedicated temp directory;
  • mount temp directory on bounded volume;
  • monitor disk usage;
  • align proxy and application limits;
  • do not rely on this as only policy.

4.1 Dedicated Temp Directory

Kubernetes example:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: evidence-service
spec:
  template:
    spec:
      containers:
        - name: app
          image: evidence-service:1.0.0
          volumeMounts:
            - name: upload-tmp
              mountPath: /var/app/upload-tmp
          env:
            - name: SPRING_SERVLET_MULTIPART_LOCATION
              value: /var/app/upload-tmp
      volumes:
        - name: upload-tmp
          emptyDir:
            sizeLimit: 2Gi

emptyDir cocok untuk scratch, bukan durable source of truth.


5. Never Trust Original Filename

Bad:

Path target = uploadDir.resolve(file.getOriginalFilename());
file.transferTo(target);

Problems:

  • ../../etc/passwd style path traversal;
  • Windows path fragments;
  • unicode spoofing;
  • extremely long filename;
  • header injection risk if reused in response;
  • overwrite existing file;
  • information leak.

Safer policy:

public final class FileNamePolicy {
    private static final int MAX_DISPLAY_LENGTH = 255;

    public String normalizeForDisplay(String original) {
        if (original == null || original.isBlank()) {
            return "unnamed";
        }

        String name = Path.of(original).getFileName().toString();
        name = name.replaceAll("[\\r\\n\\t]", "_");
        name = name.replaceAll("[^a-zA-Z0-9._ -]", "_");

        if (name.length() > MAX_DISPLAY_LENGTH) {
            name = name.substring(0, MAX_DISPLAY_LENGTH);
        }

        return name;
    }

    public String storageKey(FileId fileId) {
        return "quarantine/" + fileId.value() + "/payload";
    }
}

Better storage key:

quarantine/2026/07/05/FILE-01JZ.../payload
accepted/2026/07/05/FILE-01JZ.../payload

Generated by service, never by client.


6. Streaming with Checksum

A strong upload implementation calculates checksum while streaming. Do not read file twice if not necessary.

public final class HashingInputStream extends FilterInputStream {
    private final MessageDigest digest;
    private long count;

    public HashingInputStream(InputStream in, MessageDigest digest) {
        super(in);
        this.digest = digest;
    }

    @Override
    public int read() throws IOException {
        int b = super.read();
        if (b != -1) {
            digest.update((byte) b);
            count++;
        }
        return b;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int n = super.read(b, off, len);
        if (n > 0) {
            digest.update(b, off, n);
            count += n;
        }
        return n;
    }

    public long bytesRead() {
        return count;
    }

    public String hexDigest() {
        byte[] bytes = digest.digest();
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte value : bytes) {
            sb.append(String.format("%02x", value));
        }
        return sb.toString();
    }
}

Usage:

try (InputStream raw = multipartFile.getInputStream()) {
    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
    HashingInputStream hashing = new HashingInputStream(raw, sha256);

    StorageWriteResult result = storage.writeQuarantineObject(
        new StorageWriteRequest(fileId.value(), hashing, multipartFile.getSize(), null, tags)
    );

    String computedSha256 = hashing.hexDigest();
    long bytesRead = hashing.bytesRead();

    integrityVerifier.verify(bytesRead, computedSha256, result);
}

Caution:

  • do not call digest() before stream fully read;
  • handle partial read failure;
  • compare expected vs actual size;
  • if client sends declared checksum, treat it as claim and verify.

7. Local Staging Pattern

Sometimes you need local staging before storage:

  • scanner requires file path;
  • legacy library only accepts File;
  • content parser needs random access;
  • you need retry upload to object storage after request stream closes.

Pattern:

public StagedFile stage(MultipartFile multipartFile, FileId fileId) {
    Path tempDir = uploadProperties.tempDir();
    Path target = tempDir.resolve(fileId.value() + ".upload");

    try (InputStream in = multipartFile.getInputStream();
         OutputStream out = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW)) {
        in.transferTo(out);
    } catch (IOException ex) {
        throw new FileStagingException("Failed to stage upload", ex);
    }

    return new StagedFile(fileId, target);
}

Hardening:

Path normalized = target.normalize();
if (!normalized.startsWith(tempDir)) {
    throw new SecurityException("Invalid staging path");
}

Use CREATE_NEW to avoid overwrite.

Cleanup:

try {
    Files.deleteIfExists(staged.path());
} catch (IOException ex) {
    log.warn("Failed to delete staged upload fileId={} path={}", staged.fileId(), staged.path(), ex);
}

Local staging must be treated as ephemeral and recoverable.


8. Servlet Stack Failure Modes

8.1 Temporary Disk Full

Symptoms:

  • multipart parsing fails before controller;
  • 500/413 depending handling;
  • pod disk pressure;
  • node eviction.

Mitigation:

  • dedicated temp volume;
  • size limit;
  • monitoring;
  • cleanup;
  • smaller threshold;
  • direct-to-storage for large files.

8.2 Slow Client Upload

Symptoms:

  • servlet thread occupied;
  • connection stays open;
  • request timeout;
  • thread pool exhaustion.

Mitigation:

  • proxy request timeout;
  • max upload size;
  • direct-to-storage;
  • async/session upload;
  • rate limiting;
  • backpressure at edge.

8.3 Framework Parses Before Authorization

Some multipart parsing may happen before controller logic. Large unauthorized requests may still consume resources.

Mitigation:

  • authenticate at gateway/edge if possible;
  • reject unauthenticated large bodies early;
  • require upload session before payload;
  • use direct upload with short-lived grants;
  • apply WAF/gateway limits.

9. WebFlux Multipart Upload

Spring WebFlux uses reactive types. Instead of MultipartFile, file parts are represented with FilePart and content can be handled as reactive Flux<DataBuffer>.

Controller:

@PostMapping(value = "/v1/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseEntity<StoredFileResponse>> upload(
    @RequestPart("file") FilePart filePart,
    @RequestPart("ownerId") String ownerId,
    ServerWebExchange exchange
) {
    return userContextResolver.currentUser(exchange)
        .flatMap(actor -> uploadService.uploadReactive(filePart, ownerId, actor))
        .map(response -> ResponseEntity.status(HttpStatus.CREATED).body(response));
}

9.1 FilePart Transfer

Simple transfer:

Mono<Void> saved = filePart.transferTo(targetPath);

But direct transfer to local file is not always what you want. If you need checksum, byte counting, streaming to object storage, or content inspection, handle content().

Flux<DataBuffer> content = filePart.content();

9.2 DataBuffer Handling

DataBuffer must be handled carefully. If you manually consume buffers, release semantics can matter depending on factory/runtime.

Simplified checksum pattern:

public Mono<FileIntegrity> calculateSha256(FilePart filePart) {
    MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException ex) {
        return Mono.error(ex);
    }

    AtomicLong size = new AtomicLong();

    return filePart.content()
        .doOnNext(buffer -> {
            ByteBuffer byteBuffer = buffer.asByteBuffer();
            size.addAndGet(byteBuffer.remaining());
            digest.update(byteBuffer);
        })
        .then(Mono.fromSupplier(() -> new FileIntegrity(
            size.get(),
            toHex(digest.digest()),
            "unknown",
            Instant.now()
        )));
}

In real code, be precise with buffer release if using low-level operations. Avoid retaining DataBuffer longer than needed.


10. MVC vs WebFlux Decision

ConcernSpring MVC MultipartFileSpring WebFlux FilePart
Programming modelblockingreactive
Simplicityeasiermore complex
Large concurrent uploadscan exhaust threads if not designedbetter fit if full stack reactive
Integration with blocking SDKnaturalcan accidentally block event loop
Backpressurelimited by servlet/containernative reactive pipeline
Team familiarityoften higherrequires discipline

Important:

Using WebFlux does not automatically make upload scalable.
Blocking storage SDK or blocking file scanner inside event loop can make it worse.

If object storage client is blocking, run it on bounded scheduler or use async SDK.


11. Proxy Upload vs Direct-to-Storage

11.1 Proxy Upload Through Java Service

Pros:

  • service controls stream;
  • easy audit;
  • easy server-side checksum;
  • no exposed storage URL;
  • simpler client.

Cons:

  • service bandwidth cost;
  • servlet thread/resource usage;
  • harder for very large uploads;
  • timeout risk;
  • horizontal scale cost.

11.2 Direct-to-Storage Upload

Pros:

  • Java service avoids data plane load;
  • better for large files;
  • easier CDN/object storage optimization;
  • resumable strategy possible.

Cons:

  • URL leakage risk;
  • completion race;
  • client complexity;
  • service sees less of stream;
  • checksum/content validation must be post-upload;
  • cleanup expired sessions mandatory.

Rule:

Small/controlled/internal upload may go through service.
Large/external/high-volume upload should strongly consider direct-to-storage.

12. Multipart Field Validation

Multipart request may include both file and metadata fields. Treat all fields as untrusted.

public record MultipartUploadCommand(
    String idempotencyKey,
    String ownerType,
    String ownerId,
    String purpose,
    MultipartFile file,
    UserContext actor
) {
    public MultipartUploadCommand {
        if (idempotencyKey == null || idempotencyKey.isBlank()) {
            throw new IllegalArgumentException("Idempotency-Key is required");
        }
        if (ownerType == null || ownerType.isBlank()) {
            throw new IllegalArgumentException("ownerType is required");
        }
        if (ownerId == null || ownerId.isBlank()) {
            throw new IllegalArgumentException("ownerId is required");
        }
        if (purpose == null || purpose.isBlank()) {
            throw new IllegalArgumentException("purpose is required");
        }
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("file is required");
        }
    }
}

But constructor validation is not enough. Domain policy still needs actor, owner, purpose, tenant, environment, and config.


13. Error Handling

Multipart parsing errors may happen before your controller. Add centralized exception handling.

@RestControllerAdvice
public class FileUploadExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<ApiError> handleMaxUpload(MaxUploadSizeExceededException ex) {
        return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
            .body(ApiError.of("FILE_TOO_LARGE", "Uploaded file exceeds maximum allowed size"));
    }

    @ExceptionHandler(FilePolicyViolationException.class)
    public ResponseEntity<ApiError> handlePolicy(FilePolicyViolationException ex) {
        return ResponseEntity.unprocessableEntity()
            .body(ApiError.of(ex.reasonCode(), ex.safeMessage()));
    }

    @ExceptionHandler(FileUploadFailedException.class)
    public ResponseEntity<ApiError> handleUploadFailed(FileUploadFailedException ex) {
        return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
            .body(ApiError.of("FILE_STORAGE_FAILURE", "File could not be stored safely"));
    }
}

Do not include:

  • local temp path;
  • bucket/key;
  • raw exception stack;
  • scanner internals;
  • secret/config values.

14. Idempotent Multipart Upload

For direct multipart request, idempotency is difficult because payload may be large. Still, create-side effect must be protected.

Flow:

1. Client sends Idempotency-Key.
2. Service computes request metadata hash, not whole file hash upfront.
3. Service checks idempotency record.
4. If existing complete response: return it.
5. If existing in progress: return 409/202 with file/session reference.
6. Otherwise create upload record.
7. Stream payload.
8. Store response in idempotency record.

For large file, prefer upload session:

Idempotency applies to session creation.
Completion applies to specific upload session.

Avoid holding full payload in idempotency store.


15. Backpressure and Resource Budget

Every upload consumes:

  • network socket;
  • request parser memory;
  • temp disk;
  • servlet thread or reactive pipeline resources;
  • storage bandwidth;
  • checksum CPU;
  • DB connections for metadata if transaction held;
  • scanner queue capacity.

Define budget:

file:
  upload:
    max-file-size: 100MB
    max-concurrent-uploads: 50
    max-tenant-upload-rate: 500MB/min
    temp-dir-quota: 2GB
    scanner-queue-max-depth: 1000

Enforce concurrency:

public final class UploadConcurrencyLimiter {
    private final Semaphore semaphore;

    public UploadConcurrencyLimiter(int maxConcurrentUploads) {
        this.semaphore = new Semaphore(maxConcurrentUploads);
    }

    public <T> T runWithPermit(Supplier<T> action) {
        if (!semaphore.tryAcquire()) {
            throw new TooManyUploadsException("Too many concurrent uploads");
        }
        try {
            return action.get();
        } finally {
            semaphore.release();
        }
    }
}

This is local per instance. For tenant/global limit, use distributed rate limiting or gateway enforcement.


16. Content Inspection Placement

Where to inspect?

PlacementProsCons
During streamearly reject, less storage wasteparser complexity, slow request
After staginglibraries can use file pathtemp disk required
After object uploadscalable asyncuntrusted file stored temporarily
Scanner serviceseparation of concernsqueue delay, state complexity

Recommended baseline:

1. basic envelope validation before accepting
2. store to quarantine
3. compute checksum/size
4. async inspect/scan
5. promote only after accepted decision

17. Direct-to-Storage Session API

Start session:

@PostMapping("/v1/upload-sessions")
public ResponseEntity<UploadSessionResponse> start(@RequestBody StartUploadRequest request) {
    UserContext actor = userContextResolver.currentUser();
    StartUploadCommand command = request.toCommand(actor);
    return ResponseEntity.status(HttpStatus.CREATED).body(uploadService.startUpload(command));
}

Complete:

@PostMapping("/v1/upload-sessions/{sessionId}/complete")
public ResponseEntity<StoredFileResponse> complete(
    @PathVariable String sessionId,
    @RequestBody CompleteUploadRequest request
) {
    UserContext actor = userContextResolver.currentUser();
    return ResponseEntity.ok(uploadService.completeUpload(request.toCommand(sessionId, actor)));
}

Complete must verify:

  • session exists;
  • session belongs to actor/tenant or actor is authorized;
  • session not expired;
  • object exists;
  • object size matches expected or allowed range;
  • checksum matches if provided/required;
  • object key matches session-generated key;
  • object is still in quarantine prefix;
  • object metadata/tags match file ID/session ID;
  • session not already completed with different object.

18. Presigned URL Risk Controls

When using presigned URL/direct upload:

  • short TTL;
  • generated object key;
  • method-specific URL;
  • size constraints where provider supports;
  • expected content type/header constraints where provider supports;
  • upload only to quarantine prefix;
  • object tags include file ID/session ID;
  • completion verification mandatory;
  • cleanup expired sessions;
  • do not treat object-created event alone as acceptance.

Risk:

A presigned URL is a delegated capability.
Anyone possessing it can use it until it expires, within its constraints.

So design it like a short-lived bearer token.


19. Multipart and Transactions

Do not hold DB transaction while reading a 500MB upload.

Bad:

@Transactional
public StoredFile upload(MultipartFile file) {
    repository.insert(...);
    storage.write(file.getInputStream()); // long blocking operation inside TX
    repository.update(...);
}

Better:

1. Create upload session/metadata in short transaction.
2. Stream file outside DB transaction.
3. Verify result.
4. Update metadata in short transaction.
5. Emit outbox event in same DB transaction as metadata update.

Why?

  • long DB locks;
  • connection pool exhaustion;
  • transaction timeout;
  • poor retry semantics;
  • inconsistent cleanup on failure.

20. Observability for Multipart

Metrics:

multipart_parse_failure_total{reason}
file_upload_rejected_total{reason,purpose}
file_upload_stream_duration_seconds{purpose}
file_upload_bytes_total{purpose}
file_upload_temp_disk_used_bytes
file_upload_concurrent_active
file_upload_storage_write_duration_seconds{provider}
file_upload_checksum_mismatch_total
file_upload_session_expired_total

Logs:

INFO file.upload.initiated fileId=FILE-... ownerType=CASE purpose=EVIDENCE actor=USER-... correlationId=...
INFO file.upload.received fileId=FILE-... sizeBytes=... sha256=... status=QUARANTINED
WARN file.upload.rejected fileId=FILE-... reason=EXTENSION_NOT_ALLOWED
WARN file.upload.failed fileId=FILE-... reason=STORAGE_TIMEOUT

Never log:

  • raw file content;
  • raw local path if sensitive;
  • presigned URL;
  • Authorization header;
  • arbitrary original filename without sanitization policy.

21. Testing Multipart Upload

21.1 MVC Test

@Test
void uploadRejectsFileLargerThanPolicy() throws Exception {
    MockMultipartFile file = new MockMultipartFile(
        "file",
        "large.pdf",
        "application/pdf",
        new byte[1024]
    );

    mockMvc.perform(multipart("/v1/files")
            .file(file)
            .param("ownerType", "CASE")
            .param("ownerId", "CASE-123")
            .param("purpose", "EVIDENCE")
            .header("Idempotency-Key", "test-key"))
        .andExpect(status().isCreated());
}

Add tests for:

  • missing file;
  • empty file;
  • too many files;
  • wrong part name;
  • missing idempotency key;
  • malicious filename;
  • spoofed content type;
  • storage failure;
  • duplicate request.

21.2 WebFlux Test

Use WebTestClient for reactive endpoint and include file part. Validate status, response body, and service interaction.

21.3 Failure Injection

Simulate:

  • InputStream throws mid-read;
  • storage write times out;
  • checksum mismatch;
  • temp directory not writable;
  • temp disk full;
  • duplicate completion;
  • scanner queue unavailable;
  • request cancelled by client.

Expected:

No ACCEPTED file without verified payload.
No silent orphan without reconciliation path.
No raw exception leaked to client.
Audit/metric emitted for material failure.

22. Common Anti-Patterns

22.1 getBytes() Everywhere

Loads entire file into heap. Breaks under large file or concurrency.

22.2 Trusting getOriginalFilename()

Path traversal, overwrite, header issues.

22.3 Storing File in Database by Default

Sometimes valid, often harmful for large payloads. DB is usually better for metadata, object storage for payload. Decide based on transactional semantics, size, backup, access pattern, and compliance.

22.4 No Quarantine

Raw upload becomes immediately downloadable. Bad for malware/content-policy risk.

22.5 One Limit Only

Only setting spring.servlet.multipart.max-file-size but proxy allows larger or domain policy differs.

22.6 Transaction Around Whole Upload

DB resources held during network/storage operation.

22.7 Presigned URL Without Completion Verification

Object exists does not mean file is accepted.

22.8 Logging Presigned URL

Presigned URL is bearer capability. Logging it can leak access.


23. Production Checklist

Framework

  • Multipart max file size configured.
  • Multipart max request size configured.
  • Temp directory explicit and bounded.
  • Error handler for size/parse exceptions.
  • No getBytes() for large file path.

API

  • Idempotency-Key required for create upload.
  • Metadata and payload separated.
  • Upload session exists for large/direct mode.
  • Stable file ID returned.
  • Error response safe and stable.

Security

  • Client filename not trusted.
  • Content type not trusted.
  • Extension allowlist exists.
  • Size limit exists at multiple layers.
  • Upload requires authorization.
  • Raw file stored in quarantine first.
  • Presigned URLs short-lived and not logged.

Reliability

  • No long DB transaction around stream.
  • Partial upload state represented.
  • Storage write failure handled.
  • Reconciliation job exists.
  • Cleanup expired sessions.
  • Orphan object detection.

Observability

  • Upload bytes, duration, failure metrics.
  • Temp disk metrics.
  • Stuck session metrics.
  • Audit events for upload lifecycle.
  • Alerts for invariant violations.

24. Key Takeaways

Multipart upload di Java bukan sekadar @RequestParam MultipartFile.

Prinsip utama:

  1. Multipart is an HTTP envelope, not a trust boundary.
  2. MultipartFile may be memory-backed or disk-backed; design explicit resource limits.
  3. Never trust original filename, declared content type, or client size claim.
  4. Avoid getBytes() for production upload paths. Stream instead.
  5. Do not hold DB transactions while streaming large payloads.
  6. Use quarantine, checksum, lifecycle state, and reconciliation.
  7. WebFlux helps only when the entire path respects non-blocking/backpressure semantics.
  8. Direct-to-storage is a capability delegation; treat presigned URL like short-lived bearer access.
  9. Upload limits must align across edge, proxy, framework, domain, and storage.

Part berikutnya masuk ke file validation and content inspection: MIME sniffing, magic bytes, extension allowlist, parser risk, malware scan, quarantine policy, and safe promotion.


References

Lesson Recap

You just completed lesson 12 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.