Handling a multipart request
Armeria provides Multipart encoder and decoder built on top of
Reactive Streams.
Building a multipart request
A Multipart consists of multiple BodyParts.
Each BodyPart is divided into headers and a body. A BodyPart headers can be created simply
using ContentDisposition.
String, HttpData, or StreamMessage can be set as the body of the BodyPart.
You can also use HttpHeaders to set complex headers for BobyPart.headers().
import java.nio.file.Path;
import com.linecorp.armeria.common.ContentDisposition;
import com.linecorp.armeria.common.multipart.BodyPart;
// Create a 'Content-Disposition' header with the 'name' parameter set to 'name'.
ContentDisposition nameDisposition =
ContentDisposition.of("form-data", "name");
// Create a BodyPart with 'Content-Disposition' header and its data.
BodyPart bodyPart1 = BodyPart.of(nameDisposition, "Meri Kim");
// Create a 'Content-Disposition' header with the name parameter set to "image",
// and the filename parameter set to "profile.png"
ContentDisposition imageDisposition =
ContentDisposition.of("form-data", "image", "profile.png");
Path image = Paths.get("/path/to/image");
// Create a BodyPart with 'Content-Disposition' header and its file.
BodyPart bodyPart2 = BodyPart.of(imageDisposition, StreamMessage.of(image));
// Create a new multipart with the two body parts.
Multipart multipart = Multipart.of(bodyPart1, bodyPart2);
If we encode and print the Multipart created above,
for (HttpData httpData : multipart.toStreamMessage().collect().join()) {
System.out.print(httpData.toStringUtf8());
}
we can see how the Multipart is encoded as shown below:
--ArmeriaBoundaryEsbNVr9Z66DAIYIN
content-disposition:form-data; name="name"
content-type:text/plain
Meri Kim
--ArmeriaBoundaryEsbNVr9Z66DAIYIN
content-disposition:form-data; name="image"; filename="profile.png"
content-type:application/octet-stream
<binary-data>
--ArmeriaBoundaryEsbNVr9Z66DAIYIN--
Sending a multipart request
The Multipart created in this way can be converted to an HttpRequest through
Multipart.toHttpRequest() and transmitted to a server using a WebClient.
WebClient client = WebClient.of("https://armeria.dev");
// Encode a `Multipart` into an `HttpRequest`
HttpRequest request = multipart.toHttpRequest("/upload");
client.execute(request).aggregate()...;
Receiving a multipart request
On the server side, the multipart request sent from the client can be decoded into a Multipart
using Multipart.from().
Server.builder()
.service((ctx, req) -> {
// Decode an `HttpRequest` into a `Multipart`
Multipart multipart = Multipart.from(req);
...
})
Since Multipart does not have the actual multipart data, you can use
Multipart.bodyParts().subscribe(...) to read data little by little as needed.
If the size of the data is not large, the data can be read after being loaded into memory through
Multipart.aggregate().
// Use a `Subscriber` to read the data with backpressure.
multipart.bodyParts().subsribe(new Subscriber() {
...
});
// Read the data after aggregation.
Multipart.from(req).aggregate()
.thenAccept(multipart -> {
for (AggregatedBodyPart bodyPart : multipart.bodyParts()) {
String content = bodyPart.contentUtf8();
...
}
});
Using @Param annotation
In annotated services, a body part content of multipart/form-data can be directly mapped into a String,
Path, File, or MultipartFile using the @Param annotation.
Note that a BodyPart can be converted into a Path, File or MultipartFile only when
the BodyPart.filename() and BodyPart.name() is specified.
import java.io.File;
import java.nio.file.Path;
import com.linecorp.armeria.common.MediaTypeNames;
import com.linecorp.armeria.common.multipart.MultipartFile;
import com.linecorp.armeria.server.annotation.Consumes;
import com.linecorp.armeria.server.annotation.Post;
@Consumes(MediaTypeNames.MULTIPART_FORM_DATA)
@Post("/upload")
public HttpResponse upload(
@Param String param,
@Param File file,
@Param Path path,
@Param MultipartFile multipartFile) {
// Do something with the multipart data
...
}
ServerBuilder.multipartUploadsLocation() and the files are removed as soon as the request is
handled completely by default, i.e. when the RequestLog is complete. If you want to persist the
uploaded file, you can move the uploaded file to another folder or persistence layer before the deletion.
Alternatively, you can disable the automatic deletion of the uploaded files by setting
MultipartRemovalStrategy.NEVER to
ServerBuilder.multipartRemovalStrategy(), but please note that you must make
sure the uploaded files are deleted once they are not in use to avoid the excessive consumption of disk space.
Using @Part annotation
While @Param treats multipart parts as simple strings or files,
the @Part annotation uses the Content-Type header of each part
to determine how to deserialize the content. For example, a part with
Content-Type: application/json will be deserialized into a Java object using Jackson.
Supported types
| Target type | Behavior |
|---|---|
File, Path, MultipartFile | File upload (same as @Param) |
String | Raw text content of the part |
byte[] | Raw binary content of the part |
| Any other type | Deserialized from JSON via Jackson. The part must have Content-Type: application/json. |
Example
import com.linecorp.armeria.common.MediaTypeNames;
import com.linecorp.armeria.common.multipart.MultipartFile;
import com.linecorp.armeria.server.annotation.Consumes;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.Part;
import com.linecorp.armeria.server.annotation.Post;
@Consumes(MediaTypeNames.MULTIPART_FORM_DATA)
@Post("/upload")
public HttpResponse upload(
@Part MyBean metadata,
@Part MultipartFile file,
@Param String title) {
// 'metadata' is deserialized from a JSON part.
// 'file' is a file upload.
// 'title' is a plain text form field.
...
}
This is useful when a client sends structured data (e.g., JSON metadata) alongside
file uploads in a single multipart request. For instance, a browser might use
JavaScript's FormData API:
const formData = new FormData();
formData.append("metadata",
new Blob([JSON.stringify({name: "example"})],
{type: "application/json"}));
formData.append("file", fileInput.files[0]);
formData.append("title", "My Upload");
Container types
@Part also supports List and Set for receiving multiple parts with the same name:
@Consumes(MediaTypeNames.MULTIPART_FORM_DATA)
@Post("/upload")
public HttpResponse upload(@Part List<MyBean> items) {
...
}