Solon File Upload: UploadedFile Over MultipartFile
File upload sounds boring until you've spent an afternoon debugging MultipartException: Failed to parse multipart servlet request in production. In Spring Boot, file upload involves MultipartFile, MultipartAutoConfigura
File upload sounds boring until you've spent an afternoon debugging MultipartException: Failed to parse multipart servlet request in production.
In Spring Boot, file upload involves MultipartFile, MultipartAutoConfiguration, spring.servlet.multipart.* properties, and a silent assumption that you're running on a servlet container. It works, but it carries a lot of hidden machinery.
Solon takes a different path. The core class is UploadedFile, it lives in the web layer with no extra configuration needed, and you have explicit control over when multipart parsing happens and when temporary files get cleaned up.
The Basics: UploadedFile
When a controller method includes an UploadedFile parameter, Solon automatically triggers multipart parsing — no annotation required:
@Controller
public class FileController {
@Post
@Mapping("/upload")
public String upload(UploadedFile file) {
try {
file.transferTo(new File("/storage/uploads/" + file.name));
return "uploaded: " + file.name + " (" + file.contentSize + " bytes)";
} finally {
file.delete(); // always clean up
}
}
}
Key properties you'll use most:
| Property | Description |
|---|---|
file.name |
Full filename including extension |
file.extension |
Extension only (e.g. jpg) |
file.contentType |
MIME type |
file.contentSize |
File size in bytes |
file.content |
Raw InputStream
|
file.contentAsBytes |
byte[] |
file.isEmpty() |
Whether the upload is empty |
One thing worth noting: the framework does not auto-clean temporary files. If you skip delete(), those temp files stay on disk. The try/finally pattern above isn't optional.
Multiple Files, Same Field Name
Use UploadedFile[] when the client sends multiple files under the same field name (supported since v2.3.8):
@Post
@Mapping("/upload/batch")
public void uploadBatch(UploadedFile[] files) {
for (UploadedFile file : files) {
try {
file.transferTo(new File("/storage/" + file.name));
} finally {
file.delete();
}
}
}
When the Field Name Doesn't Match
If the form field name differs from your parameter name, use @Param:
@Post
@Mapping("/avatar")
public void setAvatar(@Param("user_avatar") UploadedFile file) {
// handles form field "user_avatar"
}
Mixed Forms: Files + Regular Fields
You can receive both file and text fields in the same request. Parameter injection handles both automatically:
@Post
@Mapping("/upload/with-meta")
public void uploadWithMeta(UploadedFile file, String description, int category) {
try {
// file: the uploaded document
// description, category: plain text form fields
saveFile(file, description, category);
} finally {
file.delete();
}
}
When There's No UploadedFile Parameter
Sometimes a multipart form has only text fields — no file attachment. In that case Solon won't trigger multipart parsing automatically. Use multipart = true on the mapping:
@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
// multipart form with text fields only
}
You can also access files manually via Context when you need more control:
@Post
@Mapping(path = "/upload/manual", multipart = true)
public void uploadManual(String username, Context ctx) {
UploadedFile file = ctx.file("attachment");
UploadedFile[] extras = ctx.files("extras");
// process...
}
Security: Controlling autoMultipart
By default, autoMultipart is true — any incoming multipart request will be parsed automatically. On a public-facing service, this means a client can send a large file to any endpoint and trigger parsing overhead.
Tighten this up with a router filter:
Solon.start(App.class, args, app -> {
app.router().filter(-1, (ctx, chain) -> {
// only parse multipart on upload paths
ctx.autoMultipart(ctx.path().startsWith("/upload"));
chain.doFilter(ctx);
});
});
For centralized temp-file cleanup (v2.7.3+), a filter also works well:
@Component
public class MultipartCleanupFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} finally {
if (ctx.isMultipartFormData()) {
ctx.filesDelete(); // cleans all uploaded temp files
}
}
}
}
Note: If you use this pattern, don't make your upload handlers async. The filter runs on request completion — async handlers may still be working when cleanup fires.
File Size Configuration
# app.yml
server:
request:
maxBodySize: 2mb # max request body (default: 2mb)
maxFileSize: 20mb # max single file size
maxHeaderSize: 8kb # max header size
fileSizeThreshold: 512kb # below this: memory; above: temp file (v3.6.0+)
The fileSizeThreshold setting (introduced in v3.6.0) automatically routes small files to memory and large files to disk. Before v3.6.0, you had useTempfile: true for forced temp-file mode — that flag is now deprecated.
Downloading Files
Returning files is equally clean. Return DownloadedFile for byte arrays or streams, or just return a java.io.File directly:
@Get
@Mapping("/download/report")
public DownloadedFile downloadReport() {
byte[] pdf = reportService.generatePdf();
return new DownloadedFile("application/pdf", pdf, "report.pdf");
}
@Get
@Mapping("/download/avatar/{userId}")
public File downloadAvatar(@Path String userId) {
return new File("/storage/avatars/" + userId + ".jpg");
}
For more control — caching, inline display, ETags:
@Get
@Mapping("/preview/logo")
public DownloadedFile previewLogo() {
DownloadedFile file = new DownloadedFile(new File("/assets/logo.png"));
file.asAttachment(false); // display inline, don't trigger download
file.cacheControl(3600); // 304 cache for 1 hour
file.eTag("logo-v3");
return file;
}
DownloadedFile also supports HTTP Range, so it works for video streaming and large file resumable downloads without any extra configuration.
No Extra Dependencies
Everything above is in solon-web — no separate multipart starter, no auto-configuration class to hunt down:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
</dependency>
Pick any server adapter (JDK HTTP, Jetty, Undertow, Grizzly, smarthttp) — they all support temp-file mode via fileSizeThreshold.
The Mental Model
The contrast with Spring is mostly about explicitness:
-
Spring Boot:
MultipartFileinjected by the servlet layer;MultipartAutoConfigurationregisters everything;spring.servlet.multipart.*configures limits;CommonsMultipartResolveroptional. -
Solon:
UploadedFileparameter → parsing triggered; temp files stay until you calldelete();autoMultipartgives you path-level control; configuration lives inserver.request.*.
For most projects the difference is minor. Where it matters is when you want to reason clearly about when parsing happens, what lives in memory vs disk, and who is responsible for cleanup. Solon makes those questions explicit instead of hiding them in auto-configuration.
Reference:
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.