Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify REST Client multipart support #40051

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}
----


=== 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,9 +1454,108 @@
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:

Check warning on line 1459 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'needs to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'needs to'.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1459, "column": 38}}}, "severity": "INFO"}

[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.

Check warning on line 1551 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1551, "column": 74}}}, "severity": "WARNING"}

Check warning on line 1551 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1551, "column": 78}}}, "severity": "INFO"}

This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property.

Check warning on line 1553 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1553, "column": 36}}}, "severity": "WARNING"}

Check warning on line 1553 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Receiving Multipart Messages'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Receiving Multipart Messages'.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1553, "column": 103}}}, "severity": "INFO"}
====

=== 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.

Check warning on line 1558 in docs/src/main/asciidoc/rest-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/rest-client.adoc", "range": {"start": {"line": 1558, "column": 42}}}, "severity": "INFO"}

[source,java]
----
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
Loading