java.net.http.HttpClient でHTTPレスポンスを受信するとリクエストがタイムアウトした

事象に遭遇したのは AdoptOpenJDK jdk-11.0.9.1+1。

## 事象発生コード

数年ぶりにJavaで開発を行うことになり、外部APIをHTTPリクエストで呼び出してレスポンスのJSONを受信する必要が出てきた。

最後にJavaで開発した時はJDK 8でHTTPリクエストを行う標準ライブラリは
`java.net.HttpURLConnection`くらいしかなかったが、
JDK 11からは`java.net.http.HttpClient`が正式に標準ライブラリ入りしていたらしいので使用することにした。

JSONからのデシリアライズはJacksonを使おうと考えたが、`HttpClient`はレスポンスボディをreactive-streamsで処理しているらしい。

最も簡単な取り扱い方だと`HttpResponse.BodyHandlers#ofString()`で一度レスポンスボディを`String`にしてからパースする方法だが、
せっかくのreactive-streamsなのでいい感じにデータ受信完了後にJSONを処理できる方法が無いか調べると
`java.net.http.HttpResponse.BodySubscribers#mapping​(HttpResponse.BodySubscriber<T>, Function<? super T,​? extends U>)`のjavadocが見つかった。

javadocの中でJacksonを利用してレスポンスボディを直接デシリアライズしてJavaオブジェクトを返すサンプルコードが書かれていたので、
そこで紹介されているコードをほぼそのまま流用させてもらうことにした。

実装は大体以下のようなコードになった。

```java
public static <T> BodySubscriber<T> jsonSubscriber(
    ObjectMapper objectMapper,
    Class<T> klazz) {

    Objects.requireNonNull(objectMapper);
    Objects.requireNonNull(klazz);

    return BodySubscribers.mapping(
          BodySubscribers.ofInputStream(),
          (InputStream bodyStream) -> {
              try (bodyStream) {
                  return objectMapper.readValue(bodyStream, klazz);
              } catch (IOException e) {
                  throw new UncheckedIOException(e);
              }
          });
}
```

上記のメソッドで生成したサブスクライバを
`java.net.http.HttpResponse.BodyHandler<T>`の実装クラスで返すことで
`HttpClient`でレスポンスを受信した時にデシリアライズが実施されるようにしていた。

```java
public class FooResponseHandler implements HttpResponse.BodyHandler<Foo> {

    private final HttpResponse.BodySubscriber<Foo> subscriber;

    public FooResponseHandler(ObjectMapper objectMapper) {
        this.subscriber = jsonSubscriber(objectMapper, Foo.class);
    }

    @Override
    public HttpResponse.BodySubscriber<Foo> apply(HttpResponse.ResponseInfo responseInfo) {
        if (responseInfo.statusCode() == 200) {
            return this.subscriber;
        }

        throw new RuntimeException("Unknown response");
    }
}
```

## 発生した問題

`HttpClient`の動作確認のために外部APIを呼び出しすると、なぜか数十秒経過後にタイムアウトが発生する。

TCPダンプを見るとAPIからのレスポンスは1秒かからず返ってきており、レスポンスボディも想定された内容だった。

しかし、`HttpClient`はタイムアウトが発生する。

デバッガーで原因個所を特定していくと、`BodySubscribers.mapping()`の第2引数に渡された
マッピング関数が実行された後でスレッドがフリーズしているらしいことが分かった。

スレッドはフリーズしていたが、HTTPリクエストタイムアウトを発生させるスレッドは動いていたようで、
そのスレッドから例外が通知されるまでの数十秒間が待ち時間になっていたようだ。

## 原因と対処

このあたりでJDKの不具合を疑い調べてみるとStack Overflowなどに情報をまとめてくれている方たちがいた。

* https://stackoverflow.com/questions/57629401/deserializing-json-using-java-11-httpclient-and-custom-bodyhandler-with-jackson
* https://bugs.openjdk.java.net/browse/JDK-8217264
* https://bugs.openjdk.java.net/browse/JDK-8217627

`HttpClient`は内部でI/O操作のための`Executor`で`CompletableFuture`を実行しているが、
HTTPレスポンスボディを処理する中でブロッキングAPIが呼び出しされると`Executor`のスレッドが枯渇してしまうことがあるらしい。

具体的にどこでスレッドが枯渇しているのかは追いかけていないが、`InputStream`からの読み取りでフリーズしているように見えたので、
レスポンスボディのバイト列を`InputStream`に追加する処理と
`InputStream`から読み取ってJSONデシリアライズする処理で競合のような状態が発生したと予想している。

対処方法は、紹介されているとおり`BodySubscriber<T>`を`BodySubscriber<Supplier<T>>`に変更するしかないと思われる。

`HttpClient`内部処理が完了した後でレスポンスボディにアクセスする様になるため、スレッドの問題が回避できる。

ただ、この方法だとHTTPレスポンスボディのデータが全て`InputStream`にバッファリングされていそうなので、
JSONデシリアライズみたいな目的の時は`HttpResponse.BodyHandlers#ofString()`を使用するのとメモリ効率などがあまり変わらない気がするのが気になる。

この不具合はJDK 13で修正されているようだが、同時にjavadocも更新されており
`BodySubscriber`ないぶでレスポンスボディの`InputStream`などにアクセスする場合は
`BodySubscriber<Supplier<T>>`を使用する方法が推奨されるようになったようだ。