I’m writing a ktor client plugin which needs to in...
# ktor
u
I’m writing a ktor client plugin which needs to inspect the response body content. Doing such simply in
onResponse()
consumes that content and makes it unavailable for other plugins (like
Logging
) and for the code using the http client. I found the
io.ktor.client.plugins.observer.ResponseObserver
class, which is also used by the client
Logging
plugin. But there is not much documentation available for it. Is there any hint how a client plugin can inspect the response body content? From the docs of the
onResponse()
hook I originally concluded, that this is supported out of the box:
There you can inspect the response in a way you want: save cookies, add logging, etc.
a
Can you please give an example where the response body is consumed?
u
of course, here is a test case for reproducing this behaviour (you can also find the demo project here)
Copy code
val DemoPlugin = createClientPlugin("Demo") {

    // The test fails, if one of response.readBytes() or response.bodyAsText() is called in this hook.
    onResponse { response ->
        val body = String(response.readBytes(), response.charset() ?: Charsets.UTF_8)
//        failure reason, if response.readBytes() is called:
//        expected:<[Hello, world!]> but was:<[]>
//        also, the body is missing in the client logging output:
//        BODY START
//
//        BODY END

//        val body = response.bodyAsText(Charsets.UTF_8)
//        failure reason, if response.bodyAsText() is called:
//        Parent job is Completed
//                kotlinx.coroutines.JobCancellationException: Parent job is Completed; job=JobImpl{Completed}@1782e8fc
//        in this case, the client log mentions the body being omitted:
//        BODY START
//        [response body omitted]
//        BODY END
    }
}

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        val client = createClient {
            install(Logging) {
                logger = Logger.DEFAULT
                level = LogLevel.ALL
            }
            install(DemoPlugin)
        }

        val response = client.get("/")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("Hello, world!", response.bodyAsText())
    }
}
The key point to reproduce this behaviour is to use either
response.readBytes()
or
response.bodyAsText()
within an
onResponse
hook of a ktor client plugin.
By using the legacy api for ktor client plugins, the following solution works:
Copy code
/**
 * This ktor client plugin reads the response body but keeps it available for the application code using the client (and for other plugins).
 * This is achieved by using the ktor internal ResponseObserver class.
 */
public class ResponseReading private constructor(
) {
    public companion object : HttpClientPlugin<Unit, ResponseReading> {
        override val key: AttributeKey<ResponseReading> = AttributeKey("ResponseReading")

        override fun prepare(block: Unit.() -> Unit): ResponseReading {
            return ResponseReading()
        }

        override fun install(plugin: ResponseReading, scope: HttpClient) {
            plugin.setupReadResponse(scope)
        }
    }

    private fun setupReadResponse(scope: HttpClient) {
        val observer: ResponseHandler = observer@{ response ->
            val body = String(response.readBytes(), response.charset() ?: Charsets.UTF_8)
            println("body as read in plugin: $body")
        }
        ResponseObserver.install(ResponseObserver(observer), scope)
    }
}
a
Since the solution with the
ResponseObserver
plugin works and you want to incorporate it in your own plugin, you can take the code of the
ResponseObserver
plugin as an example.
u
ah - you mean, I should not just use the ResponseObserver from ktor internally. But the code of ResponseObserver also relies on internal ktor apis like the
receivePipeline
. Would it really help to reimplement the observer, when it still relies on internal things? What do you think: wouldn’t it be helpful for a broader ktor user base to support this response reading behaviour within the new ktor client plugin api, e.g. by providing a new hook type?
102 Views