File uploads
A picture for a post, an avatar, a document — the user sends all of these to the server as a file upload. For this a form uses a special format, multipart/form-data: the request body is split into parts, and each part is a separate field or file. Javalin parses such a request itself and gives you the file as an UploadedFile object. Let us add image uploads to the blog.
A form for a file
In HTML the form needs the enctype attribute, and the field needs the file type:
<!-- src/main/resources/public/upload.html -->
<form method="post" action="/uploads" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*">
<button type="submit">Upload</button>
</form>The page opens at http://localhost:8080/upload.html: files from public are served as static files.
The upload controller
// src/main/java/com/example/blog/UploadController.java
package com.example.blog;
import io.javalin.http.BadRequestResponse;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import io.javalin.http.UploadedFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class UploadController {
private static final Set<String> ALLOWED = Set.of(".png", ".jpg", ".jpeg", ".gif", ".webp");
private final Path dir;
public UploadController(Path dir) {
this.dir = dir;
}
public void upload(Context ctx) throws IOException {
UploadedFile file = ctx.uploadedFile("image");
if (file == null) {
throw new BadRequestResponse("No file in the image field");
}
String name = save(file);
ctx.status(HttpStatus.CREATED).json(Map.of("url", "/uploads/" + name, "size", file.size()));
}
private String save(UploadedFile file) throws IOException {
String ext = file.extension().toLowerCase();
if (!ALLOWED.contains(ext)) {
throw new BadRequestResponse("Only images are allowed: " + ALLOWED);
}
String name = UUID.randomUUID() + ext;
try (InputStream in = file.content()) {
Files.copy(in, dir.resolve(name));
}
return name;
}
}ctx.uploadedFile("image") returns the file from the image field, or null. UploadedFile has filename(), extension() (including the dot), size(), contentType() and content() — a stream with the data.
Why not the client's file name
The client sends the file name, and you cannot trust it: it may contain ../../Main.java or the name of someone else's file. So the server checks the extension against a list of allowed ones and makes up its own name with UUID — a random unique identifier. This way a file cannot overwrite another one or escape the folder.
Wiring, the limit and serving
// src/main/java/com/example/blog/Main.java
Path uploadDir = Path.of("uploads");
Files.createDirectories(uploadDir); // main now declares throws IOException
var uploads = new UploadController(uploadDir);
// inside Javalin.create(config -> { ... })
config.jetty.multipartConfig.maxFileSize(2, SizeUnit.MB); // io.javalin.config.SizeUnit
config.staticFiles.add(files -> {
files.hostedPath = "/uploads";
files.directory = "uploads";
files.location = Location.EXTERNAL;
});
// in the apiBuilder block
post("/uploads", uploads::upload);By default the file size is not limited. maxFileSize rejects files larger than 2 MB, and maxTotalRequestSize limits the whole request. Javalin serves the uploads folder next to the project as static files from disk (Location.EXTERNAL) at /uploads/....
Trying it out
The -F option sends a multipart/form-data request, and @ before the name means "take the file from disk":
curl -i -F "image=@cat.png" http://localhost:8080/uploadsHTTP/1.1 201 Created
Content-Type: application/json
{"url":"/uploads/67e1d845-c9f1-408a-a4b4-ec15453d2692.png","size":70}
The file is available at the link from the response:
curl -I http://localhost:8080/uploads/67e1d845-c9f1-408a-a4b4-ec15453d2692.pngHTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: max-age=0
A text file gets 400 and the text Only images are allowed: [.webp, .jpeg, .jpg, .gif, .png]. Several files from one field are read with ctx.uploadedFiles("images") — it returns a list that you go through in a loop.
Common mistake
If a file is larger than the limit, Javalin cannot parse the request. The client gets an error or a dropped connection (curl: (56) Recv failure: Connection was reset), and the server log shows:
WARN io.javalin.Javalin - Uncaught exception
jakarta.servlet.ServletException: org.eclipse.jetty.http.BadMessageException: 400: bad multipart
...
Caused by: java.lang.IllegalStateException: max file size exceeded: 2097152
The number is the limit in bytes: 2 MB. If there is no error but uploadedFile still returns null, check enctype="multipart/form-data" in the form: without it the browser sends only the file name, and the answer is No file in the image field.