Dropwizard Multipart Form Quickstart
Published on:Table of Contents
The other day I had the requirement where I had to code an endpoint such that a client could submit two bodies, one containing JSON, and the other one containing XML. One could get clever and embed the XML in JSON, vice versa, or even make two requests, but the HTTP spec already has this issue solved: multipart messages. If you took a peek at the Wikipedia page or the stackoverflow question, “What is [a] HTTP multipart request”, you might be confused because there is a lot of talk about submitting forms, files, and email. These are the typical use cases, but not the only use cases. I’m here to talk about (document) the other use case: multiple bodies.
I’ll be using Dropwizard for the examples but multipart messages should work across frameworks and languages.
Step 1: Add the bundle
First we add the dependency to our pom.xml
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-forms</artifactId>
</dependency>
And then register the bundle:
@Override
public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
bootstrap.addBundle(new MultiPartBundle());
}
Step 2: Create resource endpoint
Our resource endpoint will consume JSON content and XML content. Both will
be representing the same not-null and valid Person
class. We’ll return the
difference between the ages of the JSON person and the XML person.
@POST
@Timed
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("/form-quickstart")
public int ageDifference(
@Valid @NotNull @FormDataParam("json-body") Person jsonPerson,
@Valid @NotNull @FormDataParam("xml-body") Person xmlPerson
) {
return jsonPerson.age() - xmlPerson.age();
}
Couple of notes:
- Notice the usage of pure domain objects. Removing all the annotations would reveal a very simplistic function that has zero ties to REST or HTTP. This allows for a much easier time testing endpoints because, in this case, the business logic can be thoroughly tested like any other domain object
- There is no notion that the object for the
json-body
param is even JSON. One could supply JSON for both parts or an entirely different content type that is known. - The
FormDataParam
must come last in the annotation list as Jersey interprets the last annotation specially. Otherwise, you’re going to get those nasty “No injection source found for a parameter” - While both
Person
objects are validated, their validation errors currently lacks the nice context of other*Param
annotations. See issue 1782 for more info.
Step 3: Create Test
Assuming you added dropwizard-testing
to your pom:
@ClassRule
public static final ResourceTestRule RULE = ResourceTestRule.builder()
.addResource(new PersonResource())
.addProvider(MultiPartFeature.class)
.build();
@Test
public void testResource() {
final MultiPart multiPartEntity = new FormDataMultiPart()
.field("json-body", Person.create("Nick", 24), APPLICATION_JSON_TYPE)
.field("xml-body", Person.create("Papa", 55), APPLICATION_XML_TYPE);
assertThat(RULE.client().target("/form-quickstart")
.register(MultiPartFeature.class).request()
.post(Entity.entity(multiPartEntity, multiPartEntity.getMediaType()))
.readEntity(String.class))
.isEqualTo("-31");
}
Here we have to explicitly register the Jersey MultiPartFeature
, which is
what the bundle did in step 1. It’s a tad inconvenient.
If you have a healthy dose of skepticism whether XML or JSON is really being used you can add another test where you use JSON and XML directly.
final MultiPart multiPartEntity = new FormDataMultiPart()
.field("json-body", "{ \"age\": 24, \"name\": \"Nick\" }", APPLICATION_JSON_TYPE)
.field("xml-body", "<Person><age>55</age><name>Papa</name></Person>", APPLICATION_XML_TYPE);
Step 4: Curl Example
Oftentimes across companies and teams, Dropwizard may not be persasive, so handing them the test code snippet as a way to call the API may be inappropriate. That’s why it’s good to include a curl example as an unbiased mechanism for calling the API. It is normally easy enough to work backwards from a curl example to the language or framework of choice. For the API shown earlier a request from curl would look like:
cat > /tmp/tmp-xml-body.xml <<EOF
<root>
<age>55</age>
<name>Papa</name>
</root>
EOF
curl 'http://localhost:8080/form-quickstart' \
-F 'json-body={ "age": 24, "name": "Nick" };type=application/json' \
-F 'xml-body=</tmp/tmp-xml-body.xml;type=application/xml'
Of course, it came out a little more complicated than I wanted due to my
fruitless search in how to escape <
in the -F
flag. I initially tried
embedding the XML data like the JSON data, but curl parses the -F
flag and
<
is a special character signalling to embed the file’s contents.
Specifying type=application/json
and type=application/xml
is critical as
curl interprets the snippets and adds the appropriate Content-Type
to each
part. This content type is interpreted by Dropwizard, so that it can be
deserialized correctly.
This curl statement is translated into the following request:
POST /form-quickstart HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.47.0
Accept: */*
Content-Length: 384
Expect: 100-continue
Content-Type: multipart/form-data; boundary=------------------------9193de6aa3698f01
--------------------------9193de6aa3698f01
Content-Disposition: form-data; name="json-body"
Content-Type: application/json
{ "age": 24, "name": "Nick" }
--------------------------9193de6aa3698f01
Content-Disposition: form-data; name="xml-body"
Content-Type: application/xml
<root>
<age>55</age>
<name>Papa</name>
</root>
--------------------------9193de6aa3698f01--
As an aside, I lament the documentation for the -F
flag:
lets curl emulate a filled-in form in which a user has pressed the submit button
The documentation makes it appear as if the only use case is someone filling in a form. The sheer amount of documentation missing the use case of having multiple bodies can be misleading. Many times while writing this post I was left scratching my head wondering if what I was doing was against some kind of unspoken law. If it is, feels good.
Step 5: The missing steps
There are some steps that were omitted earlier as they distracted from the main part of using multipart forms:
Since Dropwizard is a JSON first framework, one needs to add in basic XML support:
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-xml-provider</artifactId>
<version>2.7.6</version>
</dependency>
The Jackson package (for Dropwizard 1.0.2) automatically registers an XML provider on the client and server sides, so you don’t have to do anything.
The Person
class is described below.
@AutoValue
public abstract class Person {
@JsonProperty
public abstract String name();
@JsonProperty
public abstract int age();
@JsonCreator
public static Person create(
@JsonProperty("name") String name,
@JsonProperty("age") int age
) {
return new AutoValue_Person(name, age);
}
}
I can’t help it, but I really enjoy AutoValue and want its usage to spread
Comments
If you'd like to leave a comment, please email [email protected]