Ktor: Test a POST with MultiPart using TestApplicationEngine does not success or fail

Created on 9 Jun 2020  路  6Comments  路  Source: ktorio/ktor

Ktor 1.3.2, TestApplicationEngine

When running a test that sends post with multipart it runs forever and never stops.

No problem when server really runs. It's just the test.

To Reproduce

Server:

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    install(ContentNegotiation) {
    }

    routing {
        post("/") {
            val mp = try {
                call.receiveMultipart()
            } catch (e: Exception) {
                call.application.log.error("Getting multipart error"")
                null
            }

           if (mp == null) call.respond(HttpStatusCode.BadRequest, "Null value")
           else call.respond(HttpStatusCode.OK,"OK")
        }
    }
}

Test:

internal class ApplicationKtTest {
    private val boundary = "***bbb***"
    private val  sysTempDir = System.getProperty("java.io.tmpdir") ?: "/tmp"

    private val multipart = listOf(PartData.FileItem({ byteArrayOf(1, 2, 3).inputStream().asInput() }, {}, headersOf(
            HttpHeaders.ContentDisposition,
            ContentDisposition.File
                    .withParameter(ContentDisposition.Parameters.Name, "file")
                    .withParameter(ContentDisposition.Parameters.FileName, "test.jpg")
                    .toString()
    )))

    @Test
    fun testUploadApplication() = testApp {
        handlePost("/", boundary, multipart).apply {
            println("============= RESPONSE ====================")
            println(response.content)
            println("=================================")
        }
    }
}

private fun TestApplicationEngine.handlePost(uri: String,
         boundary: String,
         multipart: List<PartData.FileItem>,
         setup: TestApplicationRequest.() -> Unit = {}
): TestApplicationCall {
    return handleRequest(method = HttpMethod.Post, uri = uri)  {
        addHeader(HttpHeaders.ContentType,
                ContentType.MultiPart.FormData.withParameter("boundary", boundary).toString()
        )
        setBody(boundary, multipart)
        setup()
    }
}

private fun testApp(callback: TestApplicationEngine.() -> Unit): Unit {
    withTestApplication({ module(true) }, callback)
}

The test runs forever and gives no result. if addHeader and setBody are commented there is a response.

Expected behavior
The test should print OK

Repository
If you just want to clone a test repository.

bug triaged

All 6 comments

If the multipart is used it works

post("/") {
            var fileName = ""
            val mp = try {
                val mp = call.receiveMultipart()

                mp.forEachPart { part ->
                    when (part) {
                        is PartData.FileItem -> fileName = part.originalFileName ?: "(no file name)"
                        else -> call.application.log.info("Not a file item: ${part.name}")
                    }
                    part.dispose
                }
            } catch (e: Exception) {
                call.application.log.error("Getting multipart error", e)
                null
            }

           if (mp == null) call.respond(HttpStatusCode.BadRequest, "Null value")
           else call.respond(HttpStatusCode.OK,"File name: $fileName")
        }

I have the same issue with my HTTP PATCH request.
@razvn do you have a workaround? I didn't understand your second comment.

@neelkamath just doing

call.receiveMultipart().forEachPart { it.dispose }

is enough to make it work.

There are currently two workarounds. Note that these snippets are pseudocode.

First Workaround

@razvn's workaround is to use call.receiveMultipart().forEachPart().

So, this code won't work:

call.receiveMultipart().readPart()!!.dispose()
call.respond(HttpStatusCode.NoContent)

But this code will work:

call.receiveMultipart().forEachPart { it.dispose() }
call.respond(HttpStatusCode.NoContent)

Second Workaround

I was wondering if this was because the call waits for some sort of EOF to know when to stop executing, but couldn't find a close method on call.receiveMultipart(). However, I was able to get rid of the call to call.receiveMultipart().forEachPart(). For example:

call.receiveMultipart().readPart()!!.dispose()
multipart.readPart() // Read it an extra time even though there's only one part.
call.respond(HttpStatusCode.NoContent)

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

Fixed in 1.5.2

Was this page helpful?
0 / 5 - 0 ratings