Skip to content

Commit

Permalink
KTOR-6632 Support receiving multipart data with Ktor client
Browse files Browse the repository at this point in the history
  • Loading branch information
e5l committed Nov 6, 2024
1 parent 5b086cc commit 160615a
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 369 deletions.
14 changes: 14 additions & 0 deletions buildSrc/src/main/kotlin/test/server/tests/MultiPartFormData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package test.server.tests

import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
Expand Down Expand Up @@ -34,6 +36,18 @@ internal fun Application.multiPartFormDataTest() {
call.receiveMultipart().readPart()
call.respond(HttpStatusCode.OK)
}
post("receive") {
val multipart = MultiPartFormDataContent(
formData {
append("text", "Hello, World!")
append("file", ByteArray(1024) { it.toByte() }, Headers.build {
append(HttpHeaders.ContentDisposition, """form-data; name="file"; filename="test.bin"""")
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.toString())
})
}
)
call.respond(multipart)
}
}
}
}
1 change: 1 addition & 0 deletions ktor-client/ktor-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ kotlin.sourceSets {
commonMain {
dependencies {
api(project(":ktor-http"))
api(project(":ktor-http:ktor-http-cio"))
api(project(":ktor-shared:ktor-events"))
api(project(":ktor-shared:ktor-websocket-serialization"))
api(project(":ktor-shared:ktor-sse"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.cio.CIOMultipartDataBase
import io.ktor.http.content.*
import io.ktor.util.logging.*
import io.ktor.utils.io.*
Expand Down Expand Up @@ -111,6 +112,28 @@ public fun HttpClient.defaultTransformers() {
proceedWith(HttpResponseContainer(info, response.status))
}

MultiPartData::class -> {
val contentType =
context.response.contentType() ?: throw IllegalStateException("No content type in response")
if (!contentType.match(ContentType.MultiPart.FormData)) {
throw IllegalStateException(
"Expected multipart/form-data, got $contentType"
)
}

val rawContentType = context.response.headers[HttpHeaders.ContentType]
?: error("No content type provided for multipart")
val contentLength = context.response.headers[HttpHeaders.ContentLength]?.toLong()


val parsedResponse = HttpResponseContainer(
info,
CIOMultipartDataBase(coroutineContext, body, rawContentType, contentLength)
)

proceedWith(parsedResponse)
}

else -> null
}
if (result != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

package io.ktor.client.tests

import io.ktor.client.call.body
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.tests.utils.*
import io.ktor.http.*
import io.ktor.http.content.MultiPartData
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.utils.io.readRemaining
import kotlinx.io.*
import kotlin.test.*
import kotlin.time.*

/**
* Tests client request with multi-part form data.
Expand Down Expand Up @@ -48,4 +52,41 @@ class MultiPartFormDataTest : ClientLoader() {
assertTrue(response.status.isSuccess())
}
}

@Test
fun testReceiveMultiPartFormData() = clientTests {
test { client ->
val response = client.post("$TEST_SERVER/multipart/receive")

val multipart = response.body<MultiPartData>()
var textFound = false
var fileFound = false

multipart.forEachPart { part ->
when (part) {
is PartData.FormItem -> {
assertEquals("text", part.name)
assertEquals("Hello, World!", part.value)
textFound = true
}
is PartData.FileItem -> {
assertEquals("file", part.name)
assertEquals("test.bin", part.originalFileName)

val bytes = part.provider().readRemaining().readByteArray()
assertEquals(1024, bytes.size)
for (i in bytes.indices) {
assertEquals(i.toByte(), bytes[i])
}
fileFound = true
}
else -> fail("Unexpected part type: ${part::class.simpleName}")
}
part.dispose()
}

assertTrue(textFound, "Text part not found")
assertTrue(fileFound, "File part not found")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.coroutines.*
import kotlin.let
import kotlin.use

/**
* Represents a multipart data object that does parse and convert parts to ktor's [PartData]
Expand Down Expand Up @@ -49,7 +51,7 @@ public class CIOMultipartDataBase(
val event = events.receive()
eventToData(event)?.let { return it }
}
} catch (t: ClosedReceiveChannelException) {
} catch (_: ClosedReceiveChannelException) {
return null
}
}
Expand Down Expand Up @@ -77,13 +79,7 @@ public class CIOMultipartDataBase(

val body = part.body
if (filename == null) {
val packet = body.readRemaining() // formFieldLimit.toLong())
// if (!body.exhausted()) {
// val cause = IllegalStateException("Form field size limit exceeded: $formFieldLimit")
// body.cancel(cause)
// throw cause
// }

val packet = body.readRemaining()
packet.use {
return PartData.FormItem(it.readText(), { part.release() }, CIOHeaders(headers))
}
Expand Down
Loading

0 comments on commit 160615a

Please sign in to comment.