Java11 HttpClientでステータスコード204が返却されたときにIOExceptionスローされる


Java11から標準APIに追加された[HttpClientクラス](https://docs.oracle.com/javase/jp/11/docs/api/java.net.http/java/net/http/HttpClient.html)を使っているとき、「IOException: unexpected content length header with 204 response」がスローされることがあります。

この原因について調べ、解決方法について少し考えました。解決方法については実装したコードも一部存在します。

# 概要

- Java11から標準APIに追加されたHttpClientクラスでリクエストを送るとIOExceptionがスローされるときがある
- HTTP/1.1の仕様に従っていないレスポンスを検知したHttpClientクラスがIOExceptionをスローしていることが原因
- レスポンスを作成するサーバの修正、Java12以降にバージョンアップする、例外を握りつぶす、OkHttpによって実装する等の解決方法が考えられる
    - 注意点としてJava11でもリビジョン（11.0.7以降？）によっては解消されていること

# 現象

外部のWeb APIを叩くため、Java11から標準APIに追加された[HttpClientクラス](https://docs.oracle.com/javase/jp/11/docs/api/java.net.http/java/net/http/HttpClient.html)を使ってHTTPリクエストを作成、レスポンスを受け取ったところ、IOExceptionがスローされました。  
例外のメッセージによると、「unexpected content length header with 204 response」となっていました。

Web APIを持つサーバ側では処理が完了しており、ステータスコード204のレスポンスを返却していました。

# 原因

調べてみると、同じ内容の報告がありました。書かれていることには、HttpClientクラスでは、HTTPレスポンスのステータスコードが204のとき、レスポンスのヘッダに「Content-Length」が付与されている場合、IOExceptionをスローするようになっているということです。

-  [\[JDK-8218662\] Allow 204 responses with Content-Length:0 - Java Bug System](https://bugs.openjdk.java.net/browse/JDK-8218662)
-  [\[JDK-8214692\] HttpClient throws IOException: unexpected content length header with 204 response - Java Bug System](https://bugs.openjdk.java.net/browse/JDK-8214692)

なぜ、上述のような仕様になっているのかというと、HTTP/1.1を定義しているRFC 7230に「Content-Length」の仕様に由来しています。

- [RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing](https://tools.ietf.org/html/rfc7230#section-3.3.2)

RFC 7230によると、

> A server MUST NOT send a Content-Length header field in any response with a status code of 1xx (Informational) or 204 (No Content).

とあります。  
つまり、ステータスコード100番台と204のレスポンスのヘッダには、Content-Lengthを**つけてはならない**のです。
ステータスコード204のレスポンスはレスポンスボディが存在しません。サーバ側の実装によってはこのようなときにレスポンスヘッダに`Content-Length: 0`がつけられます。

そのため、Java11のHttpClientクラスはこの仕様に従って、ステータスコード204のレスポンスのヘッダにContent-Lengthが付与されていると例外を発生させます。
通信相手のサーバーからのレスポンスの内容を検査しているさいに例外が発生するため、通信相手のサーバーのAPIを叩いた場合、その処理は完了しています。

## Javaの対応

この件は、あまりにも厳格すぎるということでJava12で若干修正されることになりました。

Java12ではContent-Lengthが存在しない、もしくは存在してもその値が0（ゼロ）であれば例外をスローしないというものになっています。変更内容を抜粋しました。

[jdk/jdk12: 4ce47bc1fb92](http://hg.openjdk.java.net/jdk/jdk12/rev/4ce47bc1fb92)

```java
-        if (headers.firstValue("Content-length").isPresent())
+        if (headers.firstValueAsLong("Content-length").orElse(0L) != 0L)
```

また、Java11でもリビジョン（X.Y.Zのようにバージョンを表示するとき、Zにあたる）によっては修正されています。
記載によれば、11.0.7以降でも上記の修正がされており、例外はスローされません。

自分で確認した限りでは、11.0.9では修正が適用済みでしたが、11.0.4では例外がスローされました。

# 解消方法

解消方法について少し考えてみました。

レスポンスを返す側で対応できる場合、HTTP/1.1の仕様に従ってステータスコード204のレスポンスヘッダにContent-Lengthを付けないように実装を修正します。

レスポンスを受け取る側で対応するには、以下のようなかんじでしょうか。

- Java12以降のバージョンを利用する
- IOExceptionが発生しても握りつぶす
- OkHttpを利用する

## Java12以降のバージョンを利用する

Java12以降では例外が発生しないように修正されているため、バージョンアップをおこなうのも1つの手です。

ただし、業務で利用する場合などは他機能のテストが求められることもあるでしょう。また、Java11の次のLTSはJava17（2021年9月リリース予定）のため、2021年4月時点では難しいかもしれません。

##  IOExceptionが発生しても握りつぶす

Java11をそのまま使う場合には、IOExceptionが発生しても握りつぶす処理を実装しなければなりません。

同じJava11でもリビジョンにより例外の発生有無が変わるため、開発環境で発生しなくとも本番環境で発生する可能性があります。IOExceptionが発生したときの処理、発生しないときの処理どちらも実装が必要ようになります。

レスポンスヘッダContent-Lengthが付与されていることで発生したIOExceptionなのかを見分けるには、例外のメッセージを使うしかなさそうです。（他に良い方法があれば教えてください。）
メッセージは`unexpected content length header with 204 response`です。

## OkHttpを利用する

HttpClientクラスを使わず、OSSである[OkHttp](https://square.github.io/okhttp/)を利用して実装することでIOExceptionが発生しなくなります。

OkHttpはJava8以上であれば利用でで、Java11で登場する（Java9、10で試験導入されていたが）HttpClientの実装に依存していません。

実際にOkHttpで実装したところ、IOExceptionは発生しませんでした。使ったコードは[GitHub](https://github.com/s1r-J/java-httpclient)に置きました。