ハマりました・・・
現象としては、Postmanを使ってもCURLを使ってもきちんとレスポンスボディが読めるのですが、なぜかOkHttpを使うと文字化けしてしまい、ボディをデシリアライズできませんでした。
結論から言うと、gzipという暗号化処理がされていたせいでした。
これを解決するには二通りの方法があります。
- Encodingを許可しない(こっちは簡単ですがセキュリティ面でリスクがありそうです)
- GZIPで復号する
1. Encodingを許可しない
これは簡単で、リクエストヘッダーから「Accept-Encoding」を削除してしまえば大丈夫かと。
例えばInterceptorなどを用いて
val request = chain.request().newBuilder()
.removeHeader("Accept-Encoding")
.build()
val response = chain.proceed(request)
リクエストからAccept-Encoding
を取り除きました。
ここにgzipが入っていなければサーバー側は配慮してくれる場合が多そう?です。実際にFirebaseのとあるエンドポイントは暗号化せずにレスポンスしてくれました。
2. GZIPで復号する
復号方法は
val jsonFormat = Json { ignoreUnknownKeys = true }
val buffer = GzipSource(response.body!!.source()).buffer()
val bodyJson = jsonFormat.decodeFromString<ErrorResponse>(buffer.readUtf8())
こんな感じです。Kotlin Serializationを使っていますが、そこはGsonでもJacksonでも何でもオッケーです。
着目すべきはGzipSourceでresponse.body!!.source()
を囲っている部分ですね。
これによりGzipの復号化処理を行ってくれるようです。
※ちなみに暗号化されていないものを復号しようとするとまた別のエラーが出るので注意してください
ちなみにテストコードを書くときは
val responseJsonString = """
{
"error": {
"code": 400,
"message": "INVALID_ID_TOKEN",
"errors": [
{
"message": "INVALID_ID_TOKEN",
"domain": "global",
"reason": "invalid"
}
]
}
}
""".trimIndent()
/**
* レスポンスがgzipでエンコーディングされているため、エンコーディングする
*/
val buffer = Buffer()
val byteOut = ByteArrayOutputStream()
val gzipOut = GZIPOutputStream(byteOut)
gzipOut.write(responseJsonString.toByteArray())
gzipOut.close()
buffer.write(byteOut.toByteArray())
val server = MockWebServer().apply {
enqueue(
MockResponse().apply {
setResponseCode(400)
setBody(buffer)
}
)
}
↑こんな感じで、GZIPOutStreamを使ってgzipで暗号化してbufferに書き込んで、MockResponseにsetBody()すれば暗号化されたレスポンスを再現できました。
恥ずかしながらたった今初めてgzipなるものを知りました。
一体何の文字化けなんだろう?とutf-8やら何やら探りましたが、こんなものがあったとは・・・
勉強します。