Skip to content

Commit

Permalink
Merge pull request #40051 from geoand/client-multipart-doc
Browse files Browse the repository at this point in the history
Clarify REST Client multipart support
  • Loading branch information
geoand authored Apr 12, 2024
2 parents b5d7429 + 35f322f commit b2a5ea4
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 100 deletions.
199 changes: 99 additions & 100 deletions docs/src/main/asciidoc/rest-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -325,106 +325,6 @@ public interface ExtensionsService {
}
----


=== Using ClientMultipartForm

MultipartForm can be built using the Class `ClientMultipartForm` which supports building the form as needed:

`ClientMultipartForm` can be programmatically created with custom inputs and/or from `MultipartFormDataInput` and/or from custom Quarkus REST Input annotated with `@RestForm` if received.

[source, java]
----
public interface MultipartService {
@POST
@Path("/multipart")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> multipart(ClientMultipartForm dataParts); // <1>
}
----

<1> input to the method is a custom Generic `ClientMultipartForm` which matches external application api contract.


More information about this Class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`].


Build `ClientMultipartForm` from `MultipartFormDataInput` programmatically

[source, java]
----
public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1>
throws IOException {
ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2>
for (Entry<String, Collection<FormValue>> attribute : inputForm.getValues().entrySet()) {
for (FormValue fv : attribute.getValue()) {
if (fv.isFileItem()) {
final FileItem fi = fv.getFileItem();
String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE),
MediaType.APPLICATION_OCTET_STREAM);
if (fi.isInMemory()) {
multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3>
} else {
multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
fi.getFile().toString(), mediaType); // <4>
}
} else {
multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5>
}
}
}
return multiPartForm;
}
----

<1> `MultipartFormDataInput` inputForm supported by Quarkus REST (Server).
<2> Creating a `ClientMultipartForm` object to populate with various dataparts.
<3> Adding InMemory `FileItem` to `ClientMultipartForm`
<4> Adding physical `FileItem` to `ClientMultipartForm`
<5> Adding any attribute directly to `ClientMultipartForm` if not `FileItem`.

Build `ClientMultipartForm` from custom Attributes annotated with `@RestForm`

[source, java]
----
public class MultiPartPayloadFormData { // <1>
@RestForm("files")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
List<FileUpload> files;
@RestForm("jsonPayload")
@PartType(MediaType.TEXT_PLAIN)
String jsonPayload;
}
/*
* Generate ClientMultipartForm from custom attributes annotated with @RestForm
*/
public ClientMultipartForm buildClientMultipartForm(MultiPartPayloadFormData inputForm) { // <1>
ClientMultipartForm multiPartForm = ClientMultipartForm.create();
multiPartForm.attribute("jsonPayload", inputForm.getJsonPayload(), "jsonPayload"); // <2>
inputForm.getFiles().forEach(fu -> {
multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3>
});
return multiPartForm;
}
----

<1> `MultiPartPayloadFormData` custom Object created to match the API contract for calling service which needs to be converted to `ClientMultipartForm`
<2> Adding attribute `jsonPayload` directly to `ClientMultipartForm`
<3> Adding `FileUpload` objects to `ClientMultipartForm` as binaryFileUpload with contentType.

[NOTE]
====
When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode.
By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode.
This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property.
====

=== Sending large payloads

The REST Client is capable of sending arbitrarily large HTTP bodies without buffering the contents in memory, if one of the following types is used:
Expand Down Expand Up @@ -1554,6 +1454,105 @@ You can also send JSON multiparts by specifying the `@PartType` annotation:
String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person);
----

==== Programmatically creating the Multipart form

In cases where the multipart content needs to be built up programmatically, the REST Client provides `ClientMultipartForm` which can be used in the REST Client like so:

[source, java]
----
public interface MultipartService {
@POST
@Path("/multipart")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> multipart(ClientMultipartForm dataParts);
}
----


More information about this class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`].

===== Converting a received multipart object into a client request

A good example of creating `ClientMultipartForm` is one where it is created from the server's `MultipartFormDataInput` (which represents a multipart request received by xref:rest.adoc#multipart[Quarkus REST]) - the purpose being to propagate the request downstream while allowing for arbitrary modifications:

[source, java]
----
public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1>
throws IOException {
ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2>
for (Entry<String, Collection<FormValue>> attribute : inputForm.getValues().entrySet()) {
for (FormValue fv : attribute.getValue()) {
if (fv.isFileItem()) {
final FileItem fi = fv.getFileItem();
String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE),
MediaType.APPLICATION_OCTET_STREAM);
if (fi.isInMemory()) {
multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3>
} else {
multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
fi.getFile().toString(), mediaType); // <4>
}
} else {
multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5>
}
}
}
return multiPartForm;
}
----

<1> `MultipartFormDataInput` is a Quarkus REST (Server) type representing a received multipart request.
<2> A `ClientMultipartForm` is created.
<3> `FileItem` attribute is created for the request attribute that represented an in memory file attribute
<4> `FileItem` attribute is created for the request attribute that represented a file attribute saved on the file system
<5> Non-file attributes added directly to `ClientMultipartForm` if not `FileItem`.


In a similar fashion if the received server multipart request is known and looks something like:

[source, java]
----
public class Request { // <1>
@RestForm("files")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
List<FileUpload> files;
@RestForm("jsonPayload")
@PartType(MediaType.TEXT_PLAIN)
String jsonPayload;
}
----

the `ClientMultipartForm` can be created easily as follows:

[source, java]
----
public ClientMultipartForm buildClientMultipartForm(Request request) { // <1>
ClientMultipartForm multiPartForm = ClientMultipartForm.create();
multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2>
request.getFiles().forEach(fu -> {
multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3>
});
return multiPartForm;
}
----

<1> `Request` representing the request the server parts accepts
<2> A `jsonPayload` attribute is added directly to `ClientMultipartForm`
<3> A `binaryFileUpload` is created from the request's `FileUpload` (which is a Quarkus REST (Server) type used to represent a binary file upload)

[NOTE]
====
When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode.
By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode.
This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property.
====

=== Receiving Multipart Messages
REST Client also supports receiving multipart messages.
As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package org.jboss.resteasy.reactive.multipart;

/**
* Represent a file that should be pushed to the client.
* <p>
* WARNING: This type is currently only supported on the server
*/
public interface FileDownload extends FilePart {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import java.nio.file.Path;

/**
* Represent a file that has been uploaded.
* <p>
* WARNING: This type is currently only supported on the server
*/
public interface FileUpload extends FilePart {

/**
Expand Down

0 comments on commit b2a5ea4

Please sign in to comment.