9

Java 11's java.net.http.HttpClient does not seem to check the HTTP status code, except for redirects (if enabled). And all examples found in the wiki and in the Java API documentation always assume that HTTP requests are successful, i.e. they never seem to check the status code of the response.

This is most likely never the desired behavior because an error page from an HTTP 500 (server error) response has probably a different, or no format and can therefore not be handled by the application.

Where should the check for the HTTP status code occur?

The documentation of HttpResponse.BodyHandler contains the following example snippet:

BodyHandler<Path> bodyHandler = (rspInfo) -> rspInfo.statusCode() == 200
    ? BodySubscribers.ofFile(Paths.get("/tmp/f"))
    : BodySubscribers.replacing(Paths.get("/NULL"));

However, then you would have to check twice for the status code, once in the BodyHandler shown above, and once when handling the response (since trying to read the body from "/NULL" would fail).

To me it seems most reasonable to perform the HTTP status code check in the BodyHandler only, e.g.:

BodyHandler<Path> bodyHandler = (rspInfo) -> {
    if (rspInfo.statusCode() == 200) {
        return BodySubscribers.ofFile(Paths.get("/tmp/f"));
    } else {
        throw new RuntimeException("Request failed");
    }
};

However, the BodyHandler documentation does not mention whether it is allowed to throw exceptions, or how HttpClient would behave in that case.

It is also surprises me that the JDK does not seem to offer functionality for handling unsuccessful HTTP responses out of the box, or am I overlooking something?

Marcono1234
  • 5,856
  • 1
  • 25
  • 43
  • Because not all responses have a body, I presume the main status to check is the one on the response. Being able to read the status in the body handler could be just to prevent waste. – ernest_k May 13 '21 at 18:26
  • Do we have any solution other than the accepted answer? – Jin Kwon Feb 15 '22 at 03:28

2 Answers2

5

The HttpClient attempts to do a good job at catching exceptions thrown by user code - but this is not the recommended way of dealing with non 200 status. You'd be relying on unspecified behavior (though it would probably do what you expect).

If you want to return an exception in case of a status which is != 200, then my recommendation would be to write a body subscriber that:

  1. Return a failed CompletionStage (which you would have completed exceptionally with your exception)
  2. Cancel the subscription (or forwards it to a BodySubscribers.discarding() subscriber)

On the other hand - if you want a different result type for the case where status != 200 you could write a BodyHandler that returns a tuple (response, error) where response is of one type and error is of another type. Something like this:

record Response<R,T>(R response, T error) {}
static class ErrorBodyHandler<R,T> implements BodyHandler<Response<R,T>> {
    final BodyHandler<R> responseHandler;
    final BodyHandler<T> errorHandler;
    public ErrorBodyHandler(BodyHandler<R> responseHandler, BodyHandler<T> errorHandler) {
        this.responseHandler = responseHandler;
        this.errorHandler = errorHandler;
    }
    @Override
    public BodySubscriber<Response<R, T>> apply(ResponseInfo responseInfo) {
        if (responseInfo.statusCode() == 200) {
            return BodySubscribers.mapping(responseHandler.apply(responseInfo),
                    (r) -> new Response<>(r, null));
        } else {
            return BodySubscribers.mapping(errorHandler.apply(responseInfo),
                    (t) -> new Response<>(null, t));
        }
    }
}

public static void main(String[] args) throws Exception {
    var client = HttpClient.newHttpClient();
    var handler = new ErrorBodyHandler<>(BodyHandlers.ofFileDownload(Path.of(".")),
            BodyHandlers.ofString());
    var request = HttpRequest
            .newBuilder(URI.create("http://host:port/"))
            .build();
    var httpResponse = 
            client.send(request, handler);
    if (httpResponse.statusCode() == 200) {
        Path path = httpResponse.body().response();
    } else {
        String error = httpResponse.body().error();
    }
}
daniel
  • 2,665
  • 1
  • 8
  • 18
  • Could you please elaborate on (ie. provide code for) your recommendation, on how to return a failed completion stage and cancel the subscription? I find the API quite powerful, but hard to grasp and with very little "real-world" examples. – Harald K Mar 04 '22 at 15:34
  • 1
    `CompletableFuture::failedFuture` (or `CompletableFuture::failedStage`) (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html#failedFuture(java.lang.Throwable)) will let you create a failed completion stage that your `BodySubscriber` can return when `getBody()` is called. `Subscription::cancel` (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Flow.Subscription.html#cancel()) will let you cancel the subscription. You can call that in your `BodySubscriber::onSubscribe` method. – daniel Mar 07 '22 at 14:57
  • Thanks! I also found the source code for `jdk.internal.net.http.ResponseSubscribers.NullSubscriber` useful (ie. the implementation of `BodySubscribers.discarding()` and `.replacing(...)`). – Harald K Mar 07 '22 at 15:15
2

I'm astounded that the JDK documentation could be so vague on such a common and necessary use case: correctly detecting failure for an HTTP request.

After reading the API docs, poring over the discussion here, and digging into the source code; I've determined that the appropriate logic might be as simple as this:

final BodyHandler<Path> bodyHandler = responseInfo -> {
  final BodySubscriber<Path> bodySubscriber;
  final int statusCode = responseInfo.statusCode();
  if(statusCode == 200) {
    bodySubscriber = BodySubscribers.ofFile(toFile);
  } else {
    bodySubscriber = BodySubscribers.replacing(null);
    bodySubscriber.onError(new IOException("HTTP request failed with response status code "
        + statusCode + "."));
  }
  return bodySubscriber;
};
getHttpClient().send(httpRequest, bodyHandler);

Does that seem right? Am I using the API correctly? Basically I just want to discard the body, return a null Path if someone asks for it (indicating the body was not saved in a file), and result in a failed completion stage.

(The JDK really needs a better approach to this, including some pre-made classes at the very least.)

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272