掲題の通り。
なんか数年前にも調べた気がするのでまとめておく。

val client = OkHttpClient.Builder().build()
val body = AwesomeRequest()

val request = Request.Builder()
  .url("https://....")
  .header("Authorization", API_KEY)
  .post(
    moshi.adapter(AwesomeRequest::class.java).toJson(body)
      .toRequestBody("application/json".toMediaType())
  )
  .build()

val result = client.newCall(request).execute()

的なコードを書いたが、なぜかレスポンスが404になってしまう。

Ok2Curl 使ってみてもターミナルだと成功してしまうしよくわからなかったんだけど、Android Studio のネットワークプロファイラ見てたらリクエストの Content-Type が最終的に application/json; charset=utf-8 になっていることがわかった。

で、サーバ側は application/json の完全一致で見てたのでエラーになってしまっていた。

OkHttp のレポジトリを検索したら、 close 済みで対応する予定なさそうだけど issue はあった

#3081 Feature: Provide RequestBody.create WITHOUT default charset=utf-8

MIME typeの仕様を見ると type/subtype;parameter=value という仕様でパラメータを指定できるはずなので、これを許容しないサーバ側の設定が間違っているからサーバ側を修正しよう、という話らしい。

とはいえ自分の管理外のサーバなど簡単に修正できない場合もある。上の issue にも workaround として NetworkInterceptor を実装する方法が載っている。

private class FixJsonContentTypeInterceptor : Interceptor {
  override fun intercept(chain: Interceptor.Chain): Response {
    val original = chain.request()

    val fixed = original.newBuilder()
      .header("Content-Type", "application/json")
      .build()

    return chain.proceed(fixed)
  }
}

val client = OkHttpClient.Builder()
  .addNetworkInterceptor(FixJsonContentTypeInterceptor())
  .build()

のようにしてあげればよい。 RequestBodycharset=utf-8 付きの Content-Type を設定したあとに上書きした RequestBody を作り直してしまおう、という作戦。