jimble

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.