HttpClient is a new client tool class provided by JDK11 under the java.net.http package. This article will teach you how to send multipart/form-data requests through HttpClient.

multipart/form-data

The types in the MIME standard can be divided into two categories: standalone types and Multipart types.

Standalone type is a type that represents only a single file or media, indicating the classification of files for the transferred data. Examples include text, application, audio, image, video, etc. Multipart type, on the other hand, specifies that the data being transferred can be divided into several separate blocks of data, each of which can have its own separate meaning and MIME type.

Multipart/form-data is the most common subtype of the Multipart type. Most commonly used in HTTP POST requests for form data and file uploads, the multipart/form-data format was first defined in the RFC2388 specification published in 1998. This specification was superseded in 2015 by the newly released RFC7578 specification.

Java does not provide a ready-made encoding tool class for multipart/form-data, so you need to use a third-party implementation. Here we recommend using the apache open source httpmime toolkit. Next I will show a demo of using HttpClient to send a file upload request.

httpmime:

1
2
3
4
5
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
</dependency>

Demo

Server

A very simple controller that handles the file uploaded by the client, as well as the JSON and form data submitted at the same time. If the output log is correct, then the client has successfully sent the multipart/form-data request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.springboot.demo.web.controller;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.google.gson.JsonObject;

import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/upload")
@Slf4j
public class UploadController {

    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
    public Object upload(@RequestPart("logo") MultipartFile logo, 
                        @RequestPart("name") String name,
                        @RequestPart("info") JsonObject info) throws IOException {

        log.info("name = {}", name);
        log.info("info = {}", info);
        log.info("logo = contentType: {}, fileName: {}, formName: {}, size: {}", logo.getContentType(),
                logo.getOriginalFilename(), logo.getName(), logo.getSize());

        try (InputStream inputStream = logo.getInputStream()){
            StreamUtils.drain(inputStream);
        } 

        return "ok";
    }
}

Client

Use MultipartEntityBuilder to create a Multipart request body, which contains a file, a form data and a JSON data. And write to the network using a pipeline stream to avoid memory overflow due to oversized request bodies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package io.springcloud.test;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.channels.Channels;
import java.nio.channels.Pipe;
import java.nio.charset.StandardCharsets;

import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;

public class MultipartRequest {

    public static void main(String[] args) throws Exception {

        /**
        * Create a Multipart request body with MultipartEntityBuilder.
        */
        HttpEntity httpEntity = MultipartEntityBuilder.create()
                // FORM
                .addPart("name",
                        new StringBody("<Spring Cloud>",
                                ContentType.create("application/x-www-form-urlencoded", StandardCharsets.UTF_8)))
                // JSON
                .addPart("info",
                        new StringBody("{\"site\": \"https://www.springcloud.io\"}", ContentType.APPLICATION_JSON))
                // FILE
                .addBinaryBody("logo", new File("C:\\Users\\KevinBlandy\\Desktop\\logo.png"), ContentType.IMAGE_PNG,
                        "logo.png")
                .build();

        /**
        * Use pipeline streams to write the encoded data directly to the network
        * instead of caching it in memory. Because Multipart request bodies contain
        * files, they can cause memory overflows if cached in memory.
        */
        Pipe pipe = Pipe.open();

        // Pipeline streams must be used in a multi-threaded environment. Using one
        // thread for simultaneous reads and writes can lead to deadlocks.
        new Thread(() -> {
            try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) {
                // Write the encoded data to the pipeline.
                httpEntity.writeTo(outputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }).start();

        HttpClient httpClient = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder(new URI("http://localhost/upload"))
                // The Content-Type header is important, don't forget to set it.
                .header("Content-Type", httpEntity.getContentType().getValue())
                // Reads data from a pipeline stream.
                .POST(BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source()))).build();

        HttpResponse<String> responseBody = httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));

        System.out.println(responseBody.body());
    }
}

Testing

Start the server first, before running the client.

Server Side Console:

1
2
3
2022-04-27 15:52:58.834  INFO 8392 --- [  XNIO-1 task-1] i.s.d.web.controller.UploadController    : name = <Spring Cloud>
2022-04-27 15:52:58.834  INFO 8392 --- [  XNIO-1 task-1] i.s.d.web.controller.UploadController    : info = {"site":"https://www.springcloud.io"}
2022-04-27 15:52:58.834  INFO 8392 --- [  XNIO-1 task-1] i.s.d.web.controller.UploadController    : logo = contentType: image/png, fileName: logo.png, formName: logo, size: 17389

Client side console:

1
ok

As you can see, the client successfully sent the multipart/form-data request.