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
}
}
}
Enter fullscreen mode Exit fullscreen mode
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();
}
}
}
Enter fullscreen mode Exit fullscreen mode
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"
}
Enter fullscreen mode Exit fullscreen mode
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();
}
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode
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...
}
Enter fullscreen mode Exit fullscreen mode
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);
});
});
Enter fullscreen mode Exit fullscreen mode
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
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
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+)
Enter fullscreen mode Exit fullscreen mode
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");
}
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
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>
Enter fullscreen mode Exit fullscreen mode
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:
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.