Uploading multipart form data using Ktor

Upload files
In this tutorial, we will learn how to make multipart form data post calls to upload files and Http form data. But before we start actual coding, lets first understand what is “multipart”?
Multipart is an extension to MIME formats. The multipart message is list of parts, each part contains header and body. The part body can be of any media type that contains either text or binary data. The Http media type for a multipart message is multipart/form-data which is used to upload files through http forms.
Form data can be also uploaded using application/x-www-form-urlencoded content-type, but it doesn’t allow us to upload files.
When we upload Http form which set to use multipart/form-data content type then the client creates multipart messages, where each field of the form will be one part. It also saves metadata about the field context in the part headers. For example, the file part generally keeps filename in part headers.
Now we have a fair idea about the multipart, let's start by building client request programmatically. Here we will use Ktor, it is a web framework popular in Kotlin world to build connected asynchronous client-server applications.
Ktor Http client supports, making multipart requests using different methods. There are two methods as below -
  1. submitFormWithBinaryData method
  2. Directly setting the body using MultiPartFormDataContent(parts: List<PartData>) as OutgoingContent
Here, we will follow the second approach by directly setting requests body.
First, create a new Gradle project for Kotlin and add below dependencies in the build.gradle file:
compile("io.ktor", "ktor-client-core", "1.2.0")
compile("io.ktor", "ktor-client-apache", "1.2.0")
To make any Http request it requires Http Client to be initialized.
HttpClient(Apache)
Here, we are using Apache Http engine to create a client. If we want, this client can be further configured for a socket, connection timeouts, etc.
Now we have our client instantiated to make Post calls to upload multipart form data. Let define our function named uploadData.
suspend fun uploadData(
    uploadUrl: Url, 
    uploadFiles: Map<String, File>,
    texts: Map<String, String>
) {
    HttpClient(Apache).post<HttpResponse>(uploadUrl) {
        headers {
            ...               (1)
        }
        body = MultiPartFormDataContent(
            ...               (2)
        )
    }.use { response ->
        ...                   (3)
    }}
In the above function, we are making Http Post call to uploadUrl (i.e. target server URL where we want to upload the data) using our client. The other two parameters are uploadFiles: map to upload multiple files and texts: to upload form text data. If you have noticed, we are setting requests body with MultiPartFormDataContent as discussed earlier.
Here, for a better understanding purpose, we have marked function uplaodData with 3 different sections. We will see those sections in detail one by one.

Section 1:

This is nothing but the headers section for our post request. You can set headers as required, In our example, we will set Authorization and Accept header.
headers {
    append("Authorization", "XXXX")
    append("Accept", ContentType.Application.Json)
}

Section 2:

This is the main section in our function where we are actually building multipart form data messages. We will set two types of form parts in our request, file and text.
body = MultiPartFormDataContent(
    formData {
        uploadFiles.entries.forEach {
            this.appendInput(
                key = it.key,
                headers = Headers.build {
                    append(HttpHeaders.ContentDisposition, 
                        "filename=${it.value.name}")
                },
                size = it.value.length()
            ) { buildPacket { writeFully(it.value.readBytes()) } }
        }
        texts.entries.forEach {
            this.append(FormPart(it.key, it.value))
        }
    })
As you see above, we are iterating through upload files and appending it to the message. Also for files, we are passing filename, file size as extra info i.e. metadata then the actual file as binary data. For text data, we simply iterate and appending it to message as form text data.
buildPacket: create a temporary builder that provides the ability to build byte packets with no knowledge of its size in lambda.

Section 3:

This the last section, where you can choose, how to handle received response from the server after performing our upload request. We simply chose to print the response in string format on the console.
{ response ->
    println("Response is : ${response.call.receive<String>()}")
}
As we have reached the end of this tutorial. Hope, You understood.
If you are interested to explore more about Ktor framework, you can visit the link: https://ktor.io/

Comments