Hello, I have a problem with the Ktor client (Engi...
# ktor
d
Hello, I have a problem with the Ktor client (Engine JS) in the browser. I would like to upload a file, but I don't know how to convert the file or how to set the parameters. With the following code I get the error message "IllegalStateException: Unknown form content type: [object ArrayBuffer])".
Copy code
val file = (dokument as File)
val blob = dokument as Blob
val fg = httpClient.call {
                    post(url) {
                        header(HttpHeaders.ContentType, ContentType.MultiPart.FormData)
                        setBody(
                            MultiPartFormDataContent(
                                formData {
                                    append("datei", file, Headers.build {
                                        append(HttpHeaders.ContentType, dokument.type)
                                    })
                                },
                                boundary = "WebAppBoundary"
                            )
                        )
                        onUpload { bytesSentTotal, contentLength -> log.debug { "Es wurden bereits $bytesSentTotal von $contentLength Bytes hochgeladen" } }
                    }
                }
Call is an auxiliary method that catches exceptions by means of Try.
a
To send a binary part you can use the
FileReader
to read a file to an
ArrayBuffer
and then convert it to a
ByteArray
using the
Int8Array
constructor. Here is an example:
Copy code
val client = HttpClient(Js) {}
val fileField = document.getElementById("file")

document.getElementById("form")!!.addEventListener("submit", { e ->
    e.preventDefault()
    val file = (fileField as HTMLInputElement).files!![0]!!

    val reader = FileReader()
    reader.readAsArrayBuffer(file)

    reader.onload = {
        val buffer = reader.result as ArrayBuffer
        GlobalScope.launch {
            val r = <http://client.post|client.post>("<https://httpbin.org/post>") {
                setBody(
                    MultiPartFormDataContent(
                        formData {
                            append("datei", Int8Array(buffer, 0, buffer.byteLength).asDynamic() as ByteArray, Headers.build {
                                append(HttpHeaders.ContentType, "application/octet-stream")
                            })
                        },
                        boundary = "WebAppBoundary"
                    )
                )
            }
            println("Response: ${r.bodyAsText()}")
        }
    }
})
HTML:
Copy code
<form id="form">
    <input type="file" id="file">
    <input type="submit" value="Send">
</form>
Or if you already have an
ArrayBuffer
then use the following line:
Copy code
Int8Array(buffer, 0, buffer.byteLength).asDynamic() as ByteArray
d
Thank you very much, the client can now upload the file. I wouldn't have figured that out on my own. Could this be added to the documentation or the example in github (or did I just not find it?)?
But now the server is still causing problems. What am I doing wrong here?
Copy code
val multipartData = call.receiveMultipart()
                    Either.catch {
                        multipartData.forEachPart { data ->
                            when (data) {
                                is FormItem -> <http://log.info|log.info>("FormItem: ${data.name} -> ${data.value}")
                                is FileItem -> {
                                    val dateiname = data.originalFileName ?: "gutachten_${Random.nextLong()}.pdf"
                                    val contentLength = call.request.header(HttpHeaders.ContentLength)
                                        ?.toIntOrNull()
                                        .validiereNichtNull {
                                            DateiGroesseNichtAngegeben("In dem FileItem ist keine Größe hinterlegt oder diese ist falsch konfiguriert ($data")
                                        }
                                        .validiere({ it <= fgEinstellungen.maxSizeUpload }) {
                                            HochgeladeneDateiIstZuGross("Das hochgeladene FG ist zu große. Maximal erlaubt sind 10 MB ($it > ${fgEinstellungen.maxSizeUpload}")
                                        }
                                    val contentType = data.contentType
                                        .validiereNichtNull {
                                            ContenttypNichtHinterlegt("Beim Upload fehlt der ContentTyp ($data)")
                                        }
                                        .validiere({ it == ContentType.fromFileExtension(".pdf").first() }) {
                                            UnzulaessigerDateityp("Zulässig für den FG-Upload sind nur PDFs. (aktuell: $dateiname -> $contentType")
                                        }

                                    // FIXME temp Name
                                    val tmpFile = File(tmpDir, "$dateiname")
                                    data.streamProvider().transferTo(tmpFile.outputStream())

                                    <http://log.info|log.info>("Datei $dateiname ($contentType -> $contentLength) wurde hochgeladen")
                                    tmpFile.delete()
                                 }

                                else -> raise(UngueltigerFormtyp("Der Upload unterstützt nur FormItem und FileItem"))
                            }
                            data.dispose
                        }
                    }
Copy code
2023-04-26 11:53:34.633 [eventLoopGroupProxy-4-6] ppd/jce22478=a3p1oau63mhm5f+jr ERROR ktor.application - Input length = 1
io.ktor.utils.io.charsets.MalformedInputException: Input length = 1
	at io.ktor.utils.io.charsets.CharsetJVMKt.throwExceptionWrapped(CharsetJVM.kt:325)
	at io.ktor.utils.io.charsets.CharsetJVMKt.decodeImplSlow(CharsetJVM.kt:289)
	at io.ktor.utils.io.charsets.CharsetJVMKt.decodeExactBytes(CharsetJVM.kt:254)
	at io.ktor.utils.io.core.StringsKt.readTextExactBytes(Strings.kt:282)
	at io.ktor.utils.io.core.StringsKt.readTextExactBytes$default(Strings.kt:281)
	at io.ktor.utils.io.core.Input.readText(Input.kt:411)
	at io.ktor.utils.io.core.Input.readText$default(Input.kt:408)
	at io.ktor.http.cio.CIOMultipartDataBase.partToData(CIOMultipartDataBase.kt:77)
	at io.ktor.http.cio.CIOMultipartDataBase.access$partToData(CIOMultipartDataBase.kt:21)
	at io.ktor.http.cio.CIOMultipartDataBase$partToData$1.invokeSuspend(CIOMultipartDataBase.kt)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174)
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167)
...
a
The problem may be that you’re trying to read binary data as a String.
d
This is how I read the stream and save the result to a file. I had taken this from the Ktor documentation. What would be the more proper way?
Copy code
val multipartData = call.receiveMultipart()
multipartData.forEachPart { data ->
 when (data) {
   is FormItem -> <http://log.info|log.info>("FormItem: ${data.name} -> ${data.value}")
   is FileItem -> {
       val tmpFile = File(tmpDir, "$dateiname")
       data.streamProvider().transferTo(tmpFile.outputStream())
......
}
a
Please check that you’re sending the
filename
parameter for a binary part.