What is the correct way to pipe a Ktor client response to a server response?

Viewed 298

I'm trying to pipe a client response from the Ktor client library as a Ktor server response using the following code:

serviceClient.retrieveCourseZip(id, version).execute { response ->
    call.respond(object : OutgoingContent.ReadChannelContent() {
        override val contentType = response.contentType()
        override val contentLength = response.contentLength()
        override val status = response.status
        override val headers = headersOf(
            HttpHeaders.ContentDisposition to listOf("attachment; filename=\"course-$courseId.zip\"")
        )

        override fun readFrom() = response.content
    })
}

response is a io.ktor.client.statement.HttpResponse returned by HttpStatement.execute.

The reason I'm doing this is that the backend service I'm developing with Ktor is a gateway that enforces authentication for clients coming from the open internet towards services that are behind this gateway.

Most of the time this code works nicely but I'm starting to have some weird behavior where clients that are downloading large amounts of data abruptly disconnect in the middle of the download due to unexpected end of stream errors on the client side. On the server side there are no error messages whatsoever and it looks like a graceful connection close.

Is there anything that is fundamentally broken with this code? I'm running this code in Amazon EKS environments where some have zero problems while others are absolutely broken.

1 Answers

From your code, it looks like your execute method is asynchronous. Ktor, on the other hand, is built around coroutines, so you need to make the coroutine wait for execute to finish. This can be done with suspendCoroutine, for example:

val result = suspendCoroutine<OutgoingContent.ReadChannelContent> { continuation ->
    serviceClient.retrieveCourseZip(id, version).execute {
        continuation.resume(object : OutgoingContent.ReadChannelContent() { /*...*/ })
    }
}
call.respond(result)

Another option is to make execute a suspend function that will return the result itself. If asynchronous code is used inside - this can be done exactly the same way with suspendCoroutine, but you say you are using a Ktor client that has to do all requests with coroutines, so your code can probably be simplified, but it is hard to tell without looking at it.

Related