File upload#
Files arriving as multipart/form-data come out of bodyFile().
Data files = context.request().bodyFile();
Trap
Files appear in neither bodyAll() nor bodyForm(). Only in bodyFile().
The text fields sitting in the same body still come out of bodyAll(), as usual.
Receiving#
for (Object value : context.request().bodyFile().values()) {
if (value instanceof List<?> list) {
for (Object item : list) {
UploadFile uploadFile = (UploadFile) item;
received.add("%s:%s:%d".formatted(
uploadFile.name(), uploadFile.fileName(), uploadFile.fileSize()));
received.add("content=" + read(uploadFile.file().toPath()));
received.add("path=" + uploadFile.relativePath());
tempFiles.add(uploadFile.file().toPath());
}
}
}
What you pull out is an UploadFile. It is a container with no methods, and this is all it holds.
| Field | What it holds |
|---|---|
name |
The form's name attribute |
fileName |
The file name the client claimed; only the last name when it includes folders ("" when absent) |
relativePath |
The file name the client claimed, folders kept (photos/2026/a.png, separated by /). For folder uploads (<input type="file" webkitdirectory>) (since 2.5.1) |
contentType |
The Content-Type the client claimed ("" when absent) |
fileSize |
The number of bytes actually written out |
file |
The temp file (java.io.File) |
File names arrive exactly as the browser sent them: Japanese, the + in a+b.png and the % in 100%.txt are kept as is.
filename*=UTF-8''... (RFC 5987), which non-browser clients send, is read too.
A file input submitted with no file chosen (filename="") is dropped (it appears in neither bodyFile() nor bodyAll()).
Careful
Even a single file is wrapped in a List.
Cast the value from bodyFile() straight to UploadFile and it blows up.
Since several files can be sent under one name, always treating it as a list is the correct shape.
Temp files#
| Where they go | upload.temp_dir (defaults to java.io.tmpdir) |
| Name | jimble-upload-<sequence>.tmp. The original file name is not used |
| When they vanish | When the request ends (the context closes) |
Trap
You cannot touch uploadFile.file after the request has ended.
Hand the work to an async job, or store only the path in the DB and read it later,
and you grab a file that is already gone. Copy anything you want to keep, inside the handler.
Temp files do not get deleteOnExit().
If the process dies they stay behind, and we leave that to the OS cleaning out its temp directory.
Limits#
The limits apply whether or not you configure them (requirement NF-S-05).
upload {
max_file_size = 10MiB # per file
max_total_size = 10MiB # per request, in total
max_files = 20 # how many
temp_dir = "" # empty means java.io.tmpdir
}
Going over throws on the spot, and the half-written temp files are deleted too.
The messages themselves come out of jimble in Japanese; the English in parentheses is a gloss, not what the response says.
| Condition | What comes back |
|---|---|
| Too many files | 413「アップロードできるファイル数は N 件までです」("at most N files") |
| One file, or the total, over the limit | 413「アップロードのサイズが上限(N バイト)を超えています」("over the N byte limit") |
| Not readable as multipart | 400「multipart を読めませんでした」("could not read the multipart body") |
Size is checked as the data is read. Measure it after taking everything in and you have already written the excess to disk.
Careful
server.max_request_size (10MiB by default) bites first.
If upload.max_total_size is larger, startup fails — the server cuts the body
first, so the limit you wrote could never be reached.
If you want to allow large uploads, raise both.
Saving#
There is no save helper. You copy uploadFile.file with the JDK's Files.
private static String saveImage (WebContext context) throws Exception {
UploadFile uploadFile = firstFile(context);
if (uploadFile == null || uploadFile.fileSize() <= 0) {
return null;
}
String extension = extensionOf(uploadFile.fileName());
if (!ALLOWED_EXTENSIONS.contains(extension)) {
throw new io.jimble.web.http.HttpException(
400, "受け付けられない形式です: %s(%s のみ)"
.formatted(uploadFile.fileName(), String.join(" ", ALLOWED_EXTENSIONS)));
}
Path dir = Path.of(UPLOAD_DIR);
Files.createDirectories(dir);
String saved = UUID.randomUUID() + extension;
/*
* 一時ファイルはリクエストが終わると消える(要件 F-W-06)。
* 残したいものはここで移す。
*/
Files.copy(uploadFile.file().toPath(), dir.resolve(saved), StandardCopyOption.REPLACE_EXISTING);
return saved;
}
Three things matter.
| You pick the name | UUID.randomUUID() + extension |
| Check the extension against an allowlist | Reject with 400 when it fails |
Files.copy |
The original is deleted anyway, so copy or move — either is fine |
Trap
Do not use uploadFile.fileName / relativePath as the destination.
jimble rejects only . / .. segments, empty segments, drive prefixes (C:evil.jsp), control characters and characters that flip text direction (400). Everything else is exactly what the client claimed.
Overlong names, characters the OS cannot use (: or ? on Windows) and overwriting a file with the same name are not prevented.
Careful
contentType is not inspected either. It is the client's own declaration.
An executable will arrive claiming to be image/png.
Write the extension allowlist, and content inspection if you need it, yourself.
Mixing with text fields#
They sit in the same body, so we parse once and hold the result (the body can only be read once).
Parts with no file name are treated as text fields and appear in bodyAll().
Note
Text fields are read as UTF-8, always, and held entirely in memory (there is no size limit).
The only thing guarding this is server.max_request_size.
Sending files back (download)#
context.response().download(file); // attach under the real file name
context.response().download(file, "report.xlsx"); // attach under a name you choose
context.response().file(file); // inline (type detected from the content)
context.response().file(file, "image/png"); // inline (type given)
context.response().stream(in, "text/csv"); // stream
context.response().stream(in, "text/csv", length); // stream of known length
Content-Disposition is written in the RFC 5987 form (filename*=UTF-8''...),
so non-ASCII file names go through as they are.
Note
The compatibility filename="..." (the ASCII form) is not written alongside it.
Also, a space in the name becomes + (not %20).
If that bothers you, replace spaces with _ before you pass the name in.