Use Multipart Forms

http4k Core

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

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:5.37.1.1"))

    implementation("org.http4k:http4k-core")
    implementation("org.http4k:http4k-multipart")
}

Standard (non-typesafe) API

package content.howto.use_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 content.howto.use_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 content.howto.use_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()
        )
    )
    return Request(POST, "http://localhost:8000")
        .with(strictFormBody of multipartform)
}

Processing Files with a Filter and convert to standard form

package content.howto.use_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 guide.reference
        println(String(multipartFile.file.content.readBytes()))
        multipartFile.file.filename
    }.then { req: Request ->
        // this is the web-form definition - it is 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)
}

Multipart combined with typesafe contract (OpenApi)

package content.howto.use_multipart_forms


import org.http4k.contract.PreFlightExtraction
import org.http4k.contract.contract
import org.http4k.contract.meta
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.core.Body
import org.http4k.core.Method.POST
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.format.Jackson
import org.http4k.lens.MultipartFormField
import org.http4k.lens.MultipartFormFile
import org.http4k.lens.Validator.Strict
import org.http4k.lens.instant
import org.http4k.lens.multipartForm
import org.http4k.server.ApacheServer
import org.http4k.server.asServer


fun main() {
    val documentPart = MultipartFormFile.required("document")
    val ownerPart = MultipartFormField.string().required("owner")
    val signaturePart = MultipartFormField.string().instant().required("signedAt")

    val formLens = Body.multipartForm(Strict, documentPart, ownerPart, signaturePart).toLens()

    val handler = contract {
        renderer = OpenApi3(ApiInfo("My great API", "v1.0"), Jackson)
        descriptionPath = "/openapi.json"

        routes += "/api/document-upload" meta {
            summary = "Uploads a document including the owner name and when it was signed"

            // required to avoid reading the multipart stream twice!
            preFlightExtraction = PreFlightExtraction.IgnoreBody

            receiving(formLens)
            returning(OK)
        } bindContract POST to { req ->
            formLens(req).use {
                val doc = documentPart(it)
                val owner = ownerPart(it)
                val signatureDate = signaturePart(it)
                //process file...
                Response(OK).body("${doc.filename} by $owner, signed at $signatureDate")
            }
        }
    }

    /**
     * example request:
     * curl -v -H 'Content-Type: multipart/form-data' \
     *      -F owner="John Doe" \
     *      -F signedAt="2011-12-03T10:15:30Z" \
     *      -F [email protected] \
     *      http://localhost:8081/api/document-upload
     */

    handler.asServer(ApacheServer(8081)).start()
}