5

I'm trying to cancel http request via new Java 11 HttpClient.

This is my test code:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class App {

    public static void main(String... args) throws InterruptedException {
        HttpClient client = HttpClient.newBuilder().build();

        URI uri = URI.create("http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso");
        HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();

        var bodyHandler = HttpResponse.BodyHandlers.ofByteArrayConsumer(b -> System.out.println("#"));
        var future = client.sendAsync(request, bodyHandler);
        Thread.sleep(1000);

        future.cancel(true);
        System.out.println("\r\n----------CANCEL!!!------------");
        System.out.println("\r\nisCancelled: " + future.isCancelled());
        Thread.sleep(250);
    }
}

I expect, that request task will be cancelled right after future.cancel(true); line invoked. And, therefore, last printed line in console should be isCancelled: true

But, when I run this code, I see something like this:

####################################################################################################
----------CANCEL!!!------------
####
isCancelled: true
#######################################################################################################################################################

This means, that request task still running after I cancel it... So, that is the right way to cancel request?

UPD

Right way to cancel request is (As daniel suggested, + UPD2: avoiding NPE on cancel() method invoke):

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Flow.Subscription;

public class App {

    private static class SubscriberWrapper implements BodySubscriber<Void> {
        private final CountDownLatch latch;
        private final BodySubscriber<Void> subscriber;
        private Subscription subscription;

        private SubscriberWrapper(BodySubscriber<Void> subscriber, CountDownLatch latch) {
            this.subscriber = subscriber;
            this.latch = latch;
        }

        @Override
        public CompletionStage<Void> getBody() {
            return subscriber.getBody();
        }

        @Override
        public void onSubscribe(Subscription subscription) {
            subscriber.onSubscribe(subscription);
            this.subscription = subscription;
            latch.countDown();
        }

        @Override
        public void onNext(List<ByteBuffer> item) {
            subscriber.onNext(item);
        }

        @Override
        public void onError(Throwable throwable) {
            subscriber.onError(throwable);
        }

        @Override
        public void onComplete() {
            subscriber.onComplete();
        }

        public void cancel() {
            subscription.cancel();
            System.out.println("\r\n----------CANCEL!!!------------");
        }
    }

    private static class BodyHandlerWrapper implements BodyHandler<Void> {
        private final CountDownLatch latch = new CountDownLatch(1);
        private final BodyHandler<Void> handler;
        private SubscriberWrapper subscriberWrapper;

        private BodyHandlerWrapper(BodyHandler<Void> handler) {
            this.handler = handler;
        }

        @Override
        public BodySubscriber<Void> apply(ResponseInfo responseInfo) {
            subscriberWrapper = new SubscriberWrapper(handler.apply(responseInfo), latch);
            return subscriberWrapper;
        }

        public void cancel() {
            CompletableFuture.runAsync(() -> {
                try {
                    latch.await();
                    subscriberWrapper.cancel();
                } catch (InterruptedException e) {}
            });
        }
    }

    public static void main(String... args) throws InterruptedException, ExecutionException {
        HttpClient client = HttpClient.newBuilder().build();

        URI uri = URI.create("http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso");
        HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();

        var handler = HttpResponse.BodyHandlers.ofByteArrayConsumer(b -> System.out.print("#"));
        BodyHandlerWrapper handlerWrapper = new BodyHandlerWrapper(handler);

        client.sendAsync(request, handlerWrapper).thenAccept(b -> System.out.println(b.statusCode()));
        Thread.sleep(1000);
        handlerWrapper.cancel();

        System.out.println("\r\n------Invoke cancel...---------");
        Thread.sleep(2500);
    }
}
  • maybe your task is already complete when you call cancel(). did you check the boolean return of cancel()? also, why didn't you implement thenAccept() in http client to manage the fulfillement of the request? – Hichem BOUSSETTA Mar 17 '19 at 16:59
  • No, task is running, You can see "#" symbols after cancel(true) method has been invoked. I don't need implement thenAccept() method in this test code, because it really dont' any manipulatioins with responce data. It only shows, that HttpClient continue receive data after I cancel the task... – Kirill Chaykin Mar 17 '19 at 17:21
  • 1
    For your specific example, you can set timeouts on both the connection and the request, using [HttpClient.Builder.connectTimeout](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html#connectTimeout%28java.time.Duration%29) and [HttpRequest.Builder.timeout](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html#timeout%28java.time.Duration%29) respectively. These *do* work, regardless of CompletableFuture implementation. – VGR Mar 17 '19 at 18:09
  • This is just an example to demonstrate the problem... Cancellation can be initiated by user and in that case timeouts is not helpful. But I can try to understand, how timeouts work and, maybe it can be used in other way. – Kirill Chaykin Mar 17 '19 at 18:23
  • @kirill-chaykin The code you're showing above is not completely safe as there is no guarantee that the subscription will be available at the time you call `cancel`: that is, it might still be `null`. You could use a `CompletableFuture` to work around that. – daniel Mar 20 '19 at 16:53
  • Just tested and although the (updated) code seems to actually work, it only cancels the processing of the server response, not the request itself. If the cancel() method is called while the server is processing the request, nothing happens until the response actually starts being sent. I don't think this is was request cancellation means and, in my case, it is quite useless, since my goal is to interrupt a long-running server (not client) process. I really can't understand how basic functionality like real (and easy) request cancellation has been overlooked in the shiny new HTTPClient api. – Dimitris Kazakos May 11 '19 at 08:20

3 Answers3

6

You can cancel an HTTP request using the java.net.http.HttpClient API by cancelling the Flow.Subscription object that is passed to the response's BodySubscriber. It should be relatively easy to trivially wrap one of the provided BodyHandler/BodySubscriber implementations in order to get hold to the subscription object. There is unfortunately no relationship between the cancel method of the CompletableFuture returned by the client, and the cancel method of the Flow.Subscription passed to the BodySubscriber. The correct way to cancel a request is through the cancel method of the subscription.

Cancelling the subscription will work both with the synchronous (HttpClient::send) and asynchronous (HttpClient::sendAsync) methods. It will have different effects however depending on whether the request was sent through HTTP/1.1 or HTTP/2.0 (with HTTP/1.1 it will cause the connection to be closed, with HTTP/2.0 it will cause the stream to be reset). And of course it might have no effect at all if the last byte of the response was already delivered to the BodySubscriber.

Update: Since Java 16 it is possible to cancel a request by interrupting the thread that called HttpClient::send or by invoking cancel(true) on the CompletableFuture returned by HttpClient::sendAsync. This has been implemented by JDK-8245462

daniel
  • 2,665
  • 1
  • 8
  • 18
  • Thank You. This is quite unobvious, but it is work! – Kirill Chaykin Mar 20 '19 at 15:57
  • As stated above, this method just cancels response body processing (at the moment when the server starts sending it), not the request itself; it is therefore not applicable to cases where there is a long-running server-side process that needs to be interrupted before the response starts flowing. – Dimitris Kazakos May 11 '19 at 09:12
  • @DimitrisKazakos Right. Although the subscription can be cancelled before the first byte of the response is received - which can close the connection/reset the stream before the server has sent its first response byte. So it all depends on how the server reacts to these events. If the `long processing time` is spent before the server can send the response headers however, then you're right and cancelling the subscription will come too late to cancel that. – daniel May 14 '19 at 10:48
2

Synchronous VS asynchronous

The request can be sent either synchronously or asynchronously. The synchronous API blocks until the Http Response is available

HttpResponse<String> response =
      client.send(request, BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());

The asynchronous API returns immediately with a CompletableFuture that completes with the HttpResponse when it becomes available. CompletableFuture was added in Java 8 and supports composable asynchronous programming.

client.sendAsync(request, BodyHandlers.ofString())
      .thenApply(response -> { System.out.println(response.statusCode());
                               return response; } )
      .thenApply(HttpResponse::body)
      .thenAccept(System.out::println);

Future object

A Future represents the result of an asynchronous computation. Java Doc

Meaning that it's not a synchronous function and that your assumption "I expect, that request task will be cancelled right after" would be true only for synchronous method.

Check cancellation of Future object

There is a useful isCancelled() method if you want to check if your task is cancelled.

if(future.isCancelled()) {
  // Future object is cancelled, do smth
} else {
  // Future object is still running, do smth
}

sendAsync() returns a CompletableFuture object

The method sendAsync() returns a CompletableFuture. Note that a CompletableFuture implements the interface of Future.

You can do something like:

client.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
       // do action when completed;
});

In technical term, the thenAccept method adds a Consumer to be called when a response has become available.

Why cancel method over CompeletableFuture won't work

Since (unlike FutureTask) this class has no direct control over the computation that causes it to be completed, cancellation is treated as just another form of exceptional completion. Method cancel has the same effect as completeExceptionally(new CancellationException()). Method isCompletedExceptionally() can be used to determine if a CompletableFuture completed in any exceptional fashion.

In case of exceptional completion with a CompletionException, methods get() and get(long, TimeUnit) throw an ExecutionException with the same cause as held in the corresponding CompletionException. To simplify usage in most contexts, this class also defines methods join() and getNow(T) that instead throw the CompletionException directly in these cases.

In other words

The cancel() method do not employ the interrupts to do the cancellation and this is why it's not working. You should use completeExceptionally(new CancellationException())

Reference

KeyMaker00
  • 6,194
  • 2
  • 50
  • 49
  • But synchronous method HttpClient.send returns HttpResponse, not a Future... So, I can't cancel request in that case too... – Kirill Chaykin Mar 17 '19 at 16:51
  • I edited my test code: added isCancelled method invocation. And it returns true... But request still running... – Kirill Chaykin Mar 17 '19 at 17:00
  • I will complete my answer then ;) – KeyMaker00 Mar 17 '19 at 17:01
  • 2
    @KirillChaykin The problem is `CompletableFuture` doesn't interrupt the underlying execution when cancelled (see https://stackoverflow.com/questions/29013831/how-to-interrupt-underlying-execution-of-completablefuture). You could always launch your own thread and use `send` instead of `sendAsync`. The former method supports being interrupted (throws `InterruptedException`). – Slaw Mar 17 '19 at 17:17
  • Unfortunally, I still don't understund, how I can cancel the request task... future.isCancelled returns true, but, task still really working... – Kirill Chaykin Mar 17 '19 at 17:24
  • @KirillChaykin Actually, I'm not sure my "use your own thread + `send`" alternative will allow you to cancel the request either. The implementation still launches asynchronously but uses `Future.get()` to wait for the result. Interrupting the thread simply interrupts the `get()` call, not the underlying request. I'm not sure it's possible to actually _stop_ the request; you can only indicate you no longer want to wait for the response. – Slaw Mar 17 '19 at 17:40
  • This is correct. In fact, the [package documentation](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) explicitly states: “Invoking the cancel method on a CompletableFuture returned by this API may not interrupt the underlying operation…” – VGR Mar 17 '19 at 17:44
  • This is quite strange, that there is no simple way to stop long request... I can't imagine, that any production application don't need that feature... – Kirill Chaykin Mar 17 '19 at 17:53
0

At least for synchronous requests you could just interrupt the thread that is calling httpClient.send(..)

The http client then aborts the request as fast a possible and throws an InterruptedException itself.

Simon Lenz
  • 2,732
  • 5
  • 33
  • 39
  • Unfortunately, this does not seem to be the case. Although this method *seems like* it cancels the request, it just produces an InterruptedException; the request does not actually get canceled (as observed on the server side). Since the HttpClient actually uses the sendAsync code (which involves a CompletableFuture) in the sync .send method, all issues stipulated in the above answers still apply. – Dimitris Kazakos Nov 04 '19 at 17:07