Skip to content

Multipart forms

Multipart form support is provided on 2 levels:

  1. Through the creation of a MultipartFormBody which can be set on a Request.
  2. Using the Lens system, which adds the facility to define form fields in a typesafe way, and to validate form contents (in either a strict (400) or "feedback" mode).

Gradle setup

    compile group: "org.http4k", name: "http4k-core", version: "3.248.0"
    compile group: "org.http4k", name: "http4k-multipart", version: "3.248.0"

Standard (non-typesafe) API

package cookbook.multipart_forms

import org.http4k.client.ApacheClient
import org.http4k.core.ContentType
import org.http4k.core.Method.POST
import org.http4k.core.MultipartFormBody
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.lens.MultipartFormField
import org.http4k.lens.MultipartFormFile
import org.http4k.server.SunHttp
import org.http4k.server.asServer

fun main() {

    // extract the body from the request and then the fields/files from it
    val server = { r: Request ->
        val receivedForm = MultipartFormBody.from(r)
        println(receivedForm.fieldValues("field"))
        println(receivedForm.field("field2"))
        println(receivedForm.files("file"))

        Response(OK)
    }.asServer(SunHttp(8000)).start()

    // add fields and files to the multipart form body
    val body = MultipartFormBody()
        .plus("field" to "my-value")
        .plus("field2" to MultipartFormField("my-value2", listOf("my-header" to "my-value")))
        .plus("file" to MultipartFormFile("image.txt", ContentType.OCTET_STREAM, "somebinarycontent".byteInputStream()))

    // we need to set both the body AND the correct content type header on the the request
    val request = Request(POST, "http://localhost:8000")
        .header("content-type", "multipart/form-data; boundary=${body.boundary}")
        .body(body)

    println(ApacheClient()(request))

    server.stop()
}

Lens (typesafe, validating) API - reads ALL contents onto disk/memory

package cookbook.multipart_forms

import org.http4k.client.ApacheClient
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.MultipartForm
import org.http4k.lens.MultipartFormField
import org.http4k.lens.MultipartFormFile
import org.http4k.lens.Validator
import org.http4k.lens.multipartForm
import org.http4k.server.SunHttp
import org.http4k.server.asServer

data class Name(val value: String)

fun main() {
    // define fields using the standard lens syntax
    val nameField = MultipartFormField.string().map(::Name, Name::value).required("name")
    val imageFile = MultipartFormFile.optional("image")

    // add fields to a form definition, along with a validator
    val strictFormBody = Body.multipartForm(Validator.Strict, nameField, imageFile, diskThreshold = 5).toLens()

    val server = ServerFilters.CatchAll().then { r: Request ->

        // to extract the contents, we first extract the form and then extract the fields from it using the lenses
        // NOTE: we are "using" the form body here because we want to close the underlying file streams
        strictFormBody(r).use {
            println(nameField(it))
            println(imageFile(it))
        }

        Response(OK)
    }.asServer(SunHttp(8000)).start()

    // creating valid form using "with()" and setting it onto the request. The content type and boundary are
    // taken care of automatically
    val multipartform = MultipartForm().with(
        nameField of Name("rita"),
        imageFile of MultipartFormFile("image.txt", ContentType.OCTET_STREAM, "somebinarycontent".byteInputStream()))
    val validRequest = Request(POST, "http://localhost:8000").with(strictFormBody of multipartform)

    println(ApacheClient()(validRequest))

    server.stop()
}

Streaming - iterate over Multiparts

package cookbook.multipart_forms

import org.http4k.client.ApacheClient
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Method.POST
import org.http4k.core.MultipartEntity
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.multipartIterator
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.MultipartForm
import org.http4k.lens.MultipartFormField
import org.http4k.lens.MultipartFormFile
import org.http4k.lens.Validator
import org.http4k.lens.multipartForm
import org.http4k.server.SunHttp
import org.http4k.server.asServer

fun main() {

    val server = ServerFilters.CatchAll().then { r: Request ->

        // here we are iterating over the multiparts as we read them out of the input
        val fields = r.multipartIterator().asSequence().fold(emptyList<MultipartEntity.Field>()) { memo, next ->
            when (next) {
                is MultipartEntity.File -> {
                    // do something with the file right here... like stream it to another server
                    memo
                }
                is MultipartEntity.Field -> memo.plus(next)
            }
        }

        println(fields)

        Response(OK)
    }.asServer(SunHttp(8000)).start()

    println(ApacheClient()(buildMultipartRequest()))

    server.stop()
}

private fun buildMultipartRequest(): Request {
    // define fields using the standard lens syntax
    val nameField = MultipartFormField.string().map(::Name, Name::value).required("name")
    val imageFile = MultipartFormFile.optional("image")

    // add fields to a form definition, along with a validator
    val strictFormBody = Body.multipartForm(Validator.Strict, nameField, imageFile, diskThreshold = 5).toLens()

    val multipartform = MultipartForm().with(
        nameField of Name("rita"),
        imageFile of MultipartFormFile("image.txt", ContentType.OCTET_STREAM, "somebinarycontent".byteInputStream()))
    val validRequest = Request(POST, "http://localhost:8000").with(strictFormBody of multipartform)
    return validRequest
}

Processing Files with a Filter and convert to standard form

package cookbook.multipart_forms

import org.http4k.client.ApacheClient
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Method.POST
import org.http4k.core.MultipartEntity
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ProcessFiles
import org.http4k.filter.ServerFilters
import org.http4k.lens.FormField
import org.http4k.lens.MultipartForm
import org.http4k.lens.MultipartFormField
import org.http4k.lens.MultipartFormFile
import org.http4k.lens.Validator
import org.http4k.lens.multipartForm
import org.http4k.lens.webForm
import org.http4k.server.SunHttp
import org.http4k.server.asServer

data class AName(val value: String)

fun main() {

    val server = ServerFilters.ProcessFiles { multipartFile: MultipartEntity.File ->
        // do something with the file right here... like stream it to another server and return the reference
        println(String(multipartFile.file.content.readBytes()))
        multipartFile.file.filename
    }
        .then { req: Request ->
            // this is the web-form definition - it it DIFFERENT to the multipart form definition,
            // because the fields and content-type have been replaced in the ProcessFiles filter
            val nameField = FormField.map(::AName, AName::value).required("name")
            val imageFile = FormField.optional("image")
            val body = Body.webForm(Validator.Strict, nameField, imageFile).toLens()

            println(body(req))

            Response(OK)
        }.asServer(SunHttp(8000)).start()

    println(ApacheClient()(buildValidMultipartRequest()))

    server.stop()
}

private fun buildValidMultipartRequest(): Request {
    // define fields using the standard lens syntax
    val nameField = MultipartFormField.string().map(::AName, AName::value).required("name")
    val imageFile = MultipartFormFile.optional("image")

    // add fields to a form definition, along with a validator
    val strictFormBody = Body.multipartForm(Validator.Strict, nameField, imageFile, diskThreshold = 5).toLens()

    val multipartform = MultipartForm().with(
        nameField of AName("rita"),
        imageFile of MultipartFormFile("image.txt", ContentType.OCTET_STREAM, "somebinarycontent".byteInputStream()))
    return Request(POST, "http://localhost:8000").with(strictFormBody of multipartform)
}