apollo-kotlin: gzip server responses break JS client

Summary

gzip compressed responses currently break the apollo client on the js target, jvm works

it looks like the client uses the content-length of the response and truncates the decompressed bytes at this offset instead of consuming all the decompressed bytes.

background might be as described here https://slack-chats.kotlinlang.org/t/545061/i-think-i-ve-discovered-a-bug-with-gzip-client-handling-and-

Version

3.7.1

Steps to reproduce the behavior

do a graphql request to a server which has gzip encoding on, this results in

JsonEncodingException: Unterminated string at ...

There is also a bug in ktor https://youtrack.jetbrains.com/issue/KTOR-4653

however when we force the ktor version to 2.1.3, there is still another error from apollo client, which might somehow relate to this.

ApolloNetworkException: Expected 2161, actual 5899

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 2
  • Comments: 31 (19 by maintainers)

Most upvoted comments

Thanks for the details and sorry for the delay, this stuff is hairy. I was able to reproduce with this:

    val client = io.ktor.client.HttpClient(Js)

    val response = client.get("https://raw.githubusercontent.com/apollographql/apollo-kotlin/main/renovate.json")
    
    // throws IllegalStateException: Expected 112, actual 113
    println(response.body<ByteArray>())

    // works fine
    // println(response.body<String>())

It looks like a Ktor bug in the ByteArray conversion. I’ll dig more and update this issue. Thanks!

After I’ve checked the source code and documentation of ktor, I was wondering if it’s necessary to install the ContentEncoding plugin of Ktor when you instantiate the KtorHttpEngine? I’ve also tried to fiddle around with a tiny example:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.compression.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking

val client = HttpClient(CIO) {
    install(ContentEncoding) {
        gzip()
    }
}
runBlocking {
    val response = client.get("http://localhost:8082/health") {
        contentType(ContentType.Application.Json)
        // header("Accept-Encoding", "gzip")
    }
    println(response.headers)
    println(response.bodyAsText())
}

If I disable the plugin, then the request won’t contain an Accept-Encoding header, unless I explicitly set it a few lines below, but when I do so, the request fails (correctly), because the client doesn’t really expect a gzipped response. The suspicious part is that there is an Accept-Encoding header in the outgoing requests in our case in the browser, which should not be possible according to their docs, and also to the example above.

I’ll try one last thing which is trying to run the test in the browser (and not node.js). Maybe there’s something different there but appart from that, I’m not sure…

I also checked in the browser with the built-in debugger, and that contentEncoding variable has a null value runtime 😕