Files
Flash5/docs/lifecycle/request/MULTIPART.md
T

6.1 KiB

Multipart

Multipart is a lazy streaming multipart/form-data parser. It reads from req.body().stream() — the request body is never fully materialised. Text fields are buffered eagerly (they are small by definition); file part bodies are exposed as zero-copy InputStreams backed directly by the socket.


Creating a parser

Multipart mp = Multipart.of(req);

Throws IllegalArgumentException if the request is not multipart/form-data or the boundary parameter is missing. Throws IOException if the initial stream read fails (the parser skips the opening --boundary\r\n preamble on construction).


Reading text fields — field(String name)

String userId = mp.field("userId");  // null if absent or a file part
String width  = mp.field("width");

Scans forward through the stream until the named text field is found. Text parts encountered along the way are buffered eagerly. File bodies encountered along the way are drained silently (discarded without heap allocation).

Order-independent for text: once a text field has been scanned, it is cached. Calling mp.field("userId") after mp.field("width") will hit the cache if userId was encountered first in the stream.

Returns null if the field is absent or if the named part is a file (has a filename attribute). Never returns file content.


Reading file parts — file(String name)

Part avatar = mp.file("avatar");  // null if absent or a text field

Scans forward until the named file part is found. Text parts encountered along the way are buffered (accessible later via field()). Earlier file bodies encountered along the way are drained silently.

The returned Part's stream is backed by the socket. Consume it before calling any other scan method — calling mp.field(), mp.file(), or mp.parts() will drain the active stream automatically, but any bytes already read are gone.

Returns null if absent or if the named part is a text field.


Collecting multiple parts — parts(String name) and parts()

List<Part> files = mp.parts("files");  // all parts named "files"
List<Part> all   = mp.parts();         // every part in declaration order

Forces a full scan. All file bodies are materialised into byte[] arrays — accept this heap cost explicitly when calling these methods. Useful for bulk uploads where you need all files before processing any of them.

parts() must be called before any file() call if you need all file parts, since a previous file() call may have consumed some of them from the stream.


The Part object

Every part — whether text or file — is returned as a Part.

Metadata

part.name()         // field name from Content-Disposition
part.filename()     // original filename, null for text fields
part.contentType()  // declared Content-Type, null if not present
part.isFile()       // true when filename() != null

Body access

// Zero-copy stream — the default
InputStream in = part.stream();

// Explicit materialisation — opt-in heap allocation
byte[] data = part.materialize();

// UTF-8 convenience — materialises if needed, result is cached
String text = part.text();

stream()

For buffered parts (text fields and parts() results): returns a fresh ByteArrayInputStream over the cached byte[]. Repeatable, no I/O.

For streaming file parts (from file()): returns the raw bounded socket InputStream. Read it once only, and before requesting the next part.

materialize()

Reads the full part body into a byte[] and caches it. Safe to call multiple times on buffered parts (returns the same array). For streaming file parts, triggers a full socket read on the first call; subsequent calls return the cached array.

text()

Convenience over materialize(). Decodes the body as UTF-8. Result is cached.


Memory model

Operation Heap cost
Multipart.of(req) 8 KB window buffer + boundary bytes
mp.field("x") body of the text field only (~bytes of the value)
mp.file("f") none — body is a bounded socket stream
part.stream() on a file none
part.materialize() on a file full file size
mp.parts() full body of every part
Draining a file body (implicit) 8 KB reused drain buffer

Ordering constraints and safe patterns

Pattern 1 — text fields + one file (most common)

Text fields are accessible in any order. The file is streamed zero-copy.

Multipart mp     = Multipart.of(req);
String    userId = mp.field("userId");   // order doesn't matter for text
String    label  = mp.field("label");
Part      file   = mp.file("file");      // stream it after fields

Files.copy(file.stream(), destination, StandardCopyOption.REPLACE_EXISTING);
return res.body("saved " + file.filename());

Pattern 2 — echo large file (zero heap)

Part file = Multipart.of(req).file("file");
return res.chunked(file.stream());  // socket-in → chunked-out, never touches heap

Pattern 3 — bulk upload (accept the heap cost)

List<Part> files = Multipart.of(req).parts("files");
for (Part f : files) {
    Files.write(dir.resolve(f.filename()), f.materialize());
}

Pattern 4 — mixed order (file before text in the form)

If the form declares the file before a text field and you need the text first, field() will drain the file body silently to reach the text part. The file is then gone. Either reorder the form fields or use parts() if you need both.

// Form order: file, then userId
// This works — file is drained silently, userId is buffered
String userId = mp.field("userId");

// But now mp.file("file") returns null — already drained

If you need both, use parts() upfront:

List<Part> all    = mp.parts();
Part       file   = all.stream().filter(Part::isFile).findFirst().orElse(null);
String     userId = all.stream().filter(p -> "userId".equals(p.name())).findFirst()
                       .map(p -> { try { return p.text(); } catch (IOException e) { throw new UncheckedIOException(e); } })
                       .orElse(null);