Deserializing JSON using Java 11 HttpClient and custom BodyHandler with Jackson halts and will not proceed

Michał Krzywański picture Michał Krzywański · Aug 23, 2019 · Viewed 9.3k times · Source

I have a problem with deserializing JSON to custom object directly using Java 11 HttpClient::send with custom HttpResponse.BodyHandler. I came across this issue while answering this SO question.

Versions that I am using :

  • OpenJDK 11
  • Jackson 2.9.9.3

I created a simple generic JsonBodyHandler class which implements HttpResponse.BodyHandler:

public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<W> {

    private final Class<W> wClass;

    public JsonBodyHandler(Class<W> wClass) {
        this.wClass = wClass;
    }

    @Override
    public HttpResponse.BodySubscriber<W> apply(HttpResponse.ResponseInfo responseInfo) {
        return asJSON(wClass);
    }

}

the asJSON method is defined as :

public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
        HttpResponse.BodySubscriber<String> upstream = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);

        return HttpResponse.BodySubscribers.mapping(
                upstream,
                (String body) -> {
                    try {
                        ObjectMapper objectMapper = new ObjectMapper();
                        return objectMapper.readValue(body, targetType);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
            });
}

So it returns a custom HttpResponse.BodySubscriber which gets body as String and then applies mapping from JSON to given targetType The code to test it :

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {

        HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
                .header("Accept", "application/json")
                .build();

        Model model = HttpClient.newHttpClient()
                .send(request, new JsonBodyHandler<>(Model.class))
                .body();

        System.out.println(model);

}

And the Model class :

public class Model {
        private String userId;
        private String id;
        private String title;
        private boolean completed;


    //getters setters constructors toString
}

The output is as expected :

Model{userId='1', id='1', title='delectus aut autem', completed=false}

However when I change the asJSON method to read InputStream instead of String first :

public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
    HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();

    return HttpResponse.BodySubscribers.mapping(
                upstream,
                (InputStream is) -> {
                    try (InputStream stream = is) {
                        ObjectMapper objectMapper = new ObjectMapper();
                        return objectMapper.readValue(stream, targetType);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
            });
}

It hangs after invoking reading the value with ObjectMapper and it does not proceed (I have checked that it successfully gets the response from the endpoint, status code is 200) but then it just hangs. Does anyone know what might be the issue?

Answer

Michał Krzywański picture Michał Krzywański · Aug 23, 2019

I have just found this SO question which has the same problem but with GZIPInputStream. It turns out that HttpResponse.BodySubscribers.mapping is buggy and it does not work as documented. Here is the link to the official OpenJDK bug site. It was fixed for OpenJDK 13. So one workaround is to use HttpResponse.BodySubscribers::ofString instead of HttpResponse.BodySubscribers::ofInputStream as upstream for HttpResponse.BodySubscribers::mapping - it is shown how to do it in my question.

Or a better solution to this, as pointed by @daniel in comment, is to return a Supplier instead of model class:

public static <W> HttpResponse.BodySubscriber<Supplier<W>> asJSON(Class<W> targetType) {
        HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();

        return HttpResponse.BodySubscribers.mapping(
                upstream,
                inputStream -> toSupplierOfType(inputStream, targetType));
    }

    public static <W> Supplier<W> toSupplierOfType(InputStream inputStream, Class<W> targetType) {
        return () -> {
            try (InputStream stream = inputStream) {
                ObjectMapper objectMapper = new ObjectMapper();
                return objectMapper.readValue(stream, targetType);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

The JsonBodyHandler also uses Supplier:

public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<Supplier<W>> {

    private final Class<W> wClass;

    public JsonBodyHandler(Class<W> wClass) {
        this.wClass = wClass;
    }

    @Override
    public HttpResponse.BodySubscriber<Supplier<W>> apply(HttpResponse.ResponseInfo responseInfo) {
        return asJSON(wClass);
    }

}

And then we can call it like this:

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {

    HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
            .header("Accept", "application/json")
            .build();

    Model model = HttpClient.newHttpClient()
            .send(request, new JsonBodyHandler<>(Model.class))
            .body()
            .get();

    System.out.println(model);

}

This is even promoted way to do it described in OpenJDK 13 docs):

The mapping function is executed using the client's executor, and can therefore be used to map any response body type, including blocking InputStream. However, performing any blocking operation in the mapper function runs the risk of blocking the executor's thread for an unknown amount of time (at least until the blocking operation finishes), which may end up starving the executor of available threads. Therefore, in the case where mapping to the desired type might block (e.g. by reading on the InputStream), then mapping to a Supplier of the desired type and deferring the blocking operation until Supplier::get is invoked by the caller's thread should be preferred.