Path: blob/master/test/jdk/java/net/httpclient/CancelRequestTest.java
41152 views
/*1* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223/*24* @test25* @bug 8245462 822982226* @summary Tests cancelling the request.27* @library /test/lib http2/server28* @key randomness29* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters30* ReferenceTracker CancelRequestTest31* @modules java.base/sun.net.www.http32* java.net.http/jdk.internal.net.http.common33* java.net.http/jdk.internal.net.http.frame34* java.net.http/jdk.internal.net.http.hpack35* @run testng/othervm -Djdk.internal.httpclient.debug=true36* -Djdk.httpclient.enableAllMethodRetry=true37* CancelRequestTest38*/39// * -Dseed=3582896013206826205L40// * -Dseed=5784221742235559231L41import com.sun.net.httpserver.HttpServer;42import com.sun.net.httpserver.HttpsConfigurator;43import com.sun.net.httpserver.HttpsServer;44import jdk.test.lib.RandomFactory;45import jdk.test.lib.net.SimpleSSLContext;46import org.testng.ITestContext;47import org.testng.annotations.AfterClass;48import org.testng.annotations.AfterTest;49import org.testng.annotations.BeforeMethod;50import org.testng.annotations.BeforeTest;51import org.testng.annotations.DataProvider;52import org.testng.annotations.Test;5354import javax.net.ssl.SSLContext;55import java.io.IOException;56import java.io.InputStream;57import java.io.OutputStream;58import java.net.InetAddress;59import java.net.InetSocketAddress;60import java.net.URI;61import java.net.http.HttpClient;62import java.net.http.HttpConnectTimeoutException;63import java.net.http.HttpRequest;64import java.net.http.HttpResponse;65import java.net.http.HttpResponse.BodyHandler;66import java.net.http.HttpResponse.BodyHandlers;67import java.util.Iterator;68import java.util.List;69import java.util.Random;70import java.util.concurrent.CancellationException;71import java.util.concurrent.CompletableFuture;72import java.util.concurrent.ConcurrentHashMap;73import java.util.concurrent.ConcurrentMap;74import java.util.concurrent.CountDownLatch;75import java.util.concurrent.ExecutionException;76import java.util.concurrent.Executor;77import java.util.concurrent.Executors;78import java.util.concurrent.atomic.AtomicLong;79import java.util.stream.Collectors;80import java.util.stream.Stream;8182import static java.lang.System.arraycopy;83import static java.lang.System.out;84import static java.nio.charset.StandardCharsets.UTF_8;85import static org.testng.Assert.assertEquals;86import static org.testng.Assert.assertTrue;8788public class CancelRequestTest implements HttpServerAdapters {8990private static final Random random = RandomFactory.getRandom();9192SSLContext sslContext;93HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]94HttpTestServer httpsTestServer; // HTTPS/1.195HttpTestServer http2TestServer; // HTTP/2 ( h2c )96HttpTestServer https2TestServer; // HTTP/2 ( h2 )97String httpURI;98String httpsURI;99String http2URI;100String https2URI;101102static final long SERVER_LATENCY = 75;103static final int MAX_CLIENT_DELAY = 75;104static final int ITERATION_COUNT = 3;105// a shared executor helps reduce the amount of threads created by the test106static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());107static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();108static volatile boolean tasksFailed;109static final AtomicLong serverCount = new AtomicLong();110static final AtomicLong clientCount = new AtomicLong();111static final long start = System.nanoTime();112public static String now() {113long now = System.nanoTime() - start;114long secs = now / 1000_000_000;115long mill = (now % 1000_000_000) / 1000_000;116long nan = now % 1000_000;117return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);118}119120final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;121private volatile HttpClient sharedClient;122123static class TestExecutor implements Executor {124final AtomicLong tasks = new AtomicLong();125Executor executor;126TestExecutor(Executor executor) {127this.executor = executor;128}129130@Override131public void execute(Runnable command) {132long id = tasks.incrementAndGet();133executor.execute(() -> {134try {135command.run();136} catch (Throwable t) {137tasksFailed = true;138System.out.printf(now() + "Task %s failed: %s%n", id, t);139System.err.printf(now() + "Task %s failed: %s%n", id, t);140FAILURES.putIfAbsent("Task " + id, t);141throw t;142}143});144}145}146147protected boolean stopAfterFirstFailure() {148return Boolean.getBoolean("jdk.internal.httpclient.debug");149}150151@BeforeMethod152void beforeMethod(ITestContext context) {153if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) {154throw new RuntimeException("some tests failed");155}156}157158@AfterClass159static final void printFailedTests() {160out.println("\n=========================");161try {162out.printf("%n%sCreated %d servers and %d clients%n",163now(), serverCount.get(), clientCount.get());164if (FAILURES.isEmpty()) return;165out.println("Failed tests: ");166FAILURES.entrySet().forEach((e) -> {167out.printf("\t%s: %s%n", e.getKey(), e.getValue());168e.getValue().printStackTrace(out);169});170if (tasksFailed) {171System.out.println("WARNING: Some tasks failed");172}173} finally {174out.println("\n=========================\n");175}176}177178private String[] uris() {179return new String[] {180httpURI,181httpsURI,182http2URI,183https2URI,184};185}186187@DataProvider(name = "asyncurls")188public Object[][] asyncurls() {189String[] uris = uris();190Object[][] result = new Object[uris.length * 2 * 3][];191//Object[][] result = new Object[uris.length][];192int i = 0;193for (boolean mayInterrupt : List.of(true, false, true)) {194for (boolean sameClient : List.of(false, true)) {195//if (!sameClient) continue;196for (String uri : uris()) {197String path = sameClient ? "same" : "new";198path = path + (mayInterrupt ? "/interrupt" : "/nointerrupt");199result[i++] = new Object[]{uri + path, sameClient, mayInterrupt};200}201}202}203assert i == uris.length * 2 * 3;204// assert i == uris.length ;205return result;206}207208@DataProvider(name = "urls")209public Object[][] alltests() {210String[] uris = uris();211Object[][] result = new Object[uris.length * 2][];212//Object[][] result = new Object[uris.length][];213int i = 0;214for (boolean sameClient : List.of(false, true)) {215//if (!sameClient) continue;216for (String uri : uris()) {217String path = sameClient ? "same" : "new";218path = path + "/interruptThread";219result[i++] = new Object[]{uri + path, sameClient};220}221}222assert i == uris.length * 2;223// assert i == uris.length ;224return result;225}226227private HttpClient makeNewClient() {228clientCount.incrementAndGet();229return TRACKER.track(HttpClient.newBuilder()230.proxy(HttpClient.Builder.NO_PROXY)231.executor(executor)232.sslContext(sslContext)233.build());234}235236HttpClient newHttpClient(boolean share) {237if (!share) return makeNewClient();238HttpClient shared = sharedClient;239if (shared != null) return shared;240synchronized (this) {241shared = sharedClient;242if (shared == null) {243shared = sharedClient = makeNewClient();244}245return shared;246}247}248249final static String BODY = "Some string | that ? can | be split ? several | ways.";250251// should accept SSLHandshakeException because of the connectionAborter252// with http/2 and should accept Stream 5 cancelled.253// => also examine in what measure we should always254// rewrap in "Request Cancelled" when the multi exchange was aborted...255private static boolean isCancelled(Throwable t) {256while (t instanceof ExecutionException) t = t.getCause();257if (t instanceof CancellationException) return true;258if (t instanceof IOException) return String.valueOf(t).contains("Request cancelled");259out.println("Not a cancellation exception: " + t);260t.printStackTrace(out);261return false;262}263264private static void delay() {265int delay = random.nextInt(MAX_CLIENT_DELAY);266try {267System.out.println("client delay: " + delay);268Thread.sleep(delay);269} catch (InterruptedException x) {270out.println("Unexpected exception: " + x);271}272}273274@Test(dataProvider = "asyncurls")275public void testGetSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning)276throws Exception {277HttpClient client = null;278uri = uri + "/get";279out.printf("%n%s testGetSendAsync(%s, %b, %b)%n", now(), uri, sameClient, mayInterruptIfRunning);280for (int i=0; i< ITERATION_COUNT; i++) {281if (!sameClient || client == null)282client = newHttpClient(sameClient);283284HttpRequest req = HttpRequest.newBuilder(URI.create(uri))285.GET()286.build();287BodyHandler<String> handler = BodyHandlers.ofString();288CountDownLatch latch = new CountDownLatch(1);289CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);290var cf1 = response.whenComplete((r,t) -> System.out.println(t));291CompletableFuture<HttpResponse<String>> cf2 = cf1.whenComplete((r,t) -> latch.countDown());292out.println("response: " + response);293out.println("cf1: " + cf1);294out.println("cf2: " + cf2);295delay();296cf1.cancel(mayInterruptIfRunning);297out.println("response after cancel: " + response);298out.println("cf1 after cancel: " + cf1);299out.println("cf2 after cancel: " + cf2);300try {301String body = cf2.get().body();302assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));303throw new AssertionError("Expected CancellationException not received");304} catch (ExecutionException x) {305out.println("Got expected exception: " + x);306assertTrue(isCancelled(x));307}308309// Cancelling the request may cause an IOException instead...310boolean hasCancellationException = false;311try {312cf1.get();313} catch (CancellationException | ExecutionException x) {314out.println("Got expected exception: " + x);315assertTrue(isCancelled(x));316hasCancellationException = x instanceof CancellationException;317}318319// because it's cf1 that was cancelled then response might not have320// completed yet - so wait for it here...321try {322String body = response.get().body();323assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));324if (mayInterruptIfRunning) {325// well actually - this could happen... In which case we'll need to326// increase the latency in the server handler...327throw new AssertionError("Expected Exception not received");328}329} catch (ExecutionException x) {330assertEquals(response.isDone(), true);331Throwable wrapped = x.getCause();332assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()));333Throwable cause = wrapped.getCause();334out.println("CancellationException cause: " + x);335assertTrue(IOException.class.isAssignableFrom(cause.getClass()));336if (cause instanceof HttpConnectTimeoutException) {337cause.printStackTrace(out);338throw new RuntimeException("Unexpected timeout exception", cause);339}340if (mayInterruptIfRunning) {341out.println("Got expected exception: " + wrapped);342out.println("\tcause: " + cause);343} else {344out.println("Unexpected exception: " + wrapped);345wrapped.printStackTrace(out);346throw x;347}348}349350assertEquals(response.isDone(), true);351assertEquals(response.isCancelled(), false);352assertEquals(cf1.isCancelled(), hasCancellationException);353assertEquals(cf2.isDone(), true);354assertEquals(cf2.isCancelled(), false);355assertEquals(latch.getCount(), 0);356}357}358359@Test(dataProvider = "asyncurls")360public void testPostSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning)361throws Exception {362uri = uri + "/post";363HttpClient client = null;364out.printf("%n%s testPostSendAsync(%s, %b, %b)%n", now(), uri, sameClient, mayInterruptIfRunning);365for (int i=0; i< ITERATION_COUNT; i++) {366if (!sameClient || client == null)367client = newHttpClient(sameClient);368369CompletableFuture<CompletableFuture<?>> cancelFuture = new CompletableFuture<>();370371Iterable<byte[]> iterable = new Iterable<byte[]>() {372@Override373public Iterator<byte[]> iterator() {374// this is dangerous375out.println("waiting for completion on: " + cancelFuture);376boolean async = random.nextBoolean();377Runnable cancel = () -> {378out.println("Cancelling from " + Thread.currentThread());379var cf1 = cancelFuture.join();380cf1.cancel(mayInterruptIfRunning);381out.println("cancelled " + cf1);382};383if (async) executor.execute(cancel);384else cancel.run();385return List.of(BODY.getBytes(UTF_8)).iterator();386}387};388389HttpRequest req = HttpRequest.newBuilder(URI.create(uri))390.POST(HttpRequest.BodyPublishers.ofByteArrays(iterable))391.build();392BodyHandler<String> handler = BodyHandlers.ofString();393CountDownLatch latch = new CountDownLatch(1);394CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);395var cf1 = response.whenComplete((r,t) -> System.out.println(t));396CompletableFuture<HttpResponse<String>> cf2 = cf1.whenComplete((r,t) -> latch.countDown());397out.println("response: " + response);398out.println("cf1: " + cf1);399out.println("cf2: " + cf2);400cancelFuture.complete(cf1);401out.println("response after cancel: " + response);402out.println("cf1 after cancel: " + cf1);403out.println("cf2 after cancel: " + cf2);404try {405String body = cf2.get().body();406assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));407throw new AssertionError("Expected CancellationException not received");408} catch (ExecutionException x) {409out.println("Got expected exception: " + x);410assertTrue(isCancelled(x));411}412413// Cancelling the request may cause an IOException instead...414boolean hasCancellationException = false;415try {416cf1.get();417} catch (CancellationException | ExecutionException x) {418out.println("Got expected exception: " + x);419assertTrue(isCancelled(x));420hasCancellationException = x instanceof CancellationException;421}422423// because it's cf1 that was cancelled then response might not have424// completed yet - so wait for it here...425try {426String body = response.get().body();427assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));428if (mayInterruptIfRunning) {429// well actually - this could happen... In which case we'll need to430// increase the latency in the server handler...431throw new AssertionError("Expected Exception not received");432}433} catch (ExecutionException x) {434assertEquals(response.isDone(), true);435Throwable wrapped = x.getCause();436assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()));437Throwable cause = wrapped.getCause();438assertTrue(IOException.class.isAssignableFrom(cause.getClass()));439if (cause instanceof HttpConnectTimeoutException) {440cause.printStackTrace(out);441throw new RuntimeException("Unexpected timeout exception", cause);442}443if (mayInterruptIfRunning) {444out.println("Got expected exception: " + wrapped);445out.println("\tcause: " + cause);446} else {447out.println("Unexpected exception: " + wrapped);448wrapped.printStackTrace(out);449throw x;450}451}452453assertEquals(response.isDone(), true);454assertEquals(response.isCancelled(), false);455assertEquals(cf1.isCancelled(), hasCancellationException);456assertEquals(cf2.isDone(), true);457assertEquals(cf2.isCancelled(), false);458assertEquals(latch.getCount(), 0);459}460}461462@Test(dataProvider = "urls")463public void testPostInterrupt(String uri, boolean sameClient)464throws Exception {465HttpClient client = null;466out.printf("%n%s testPostInterrupt(%s, %b)%n", now(), uri, sameClient);467for (int i=0; i< ITERATION_COUNT; i++) {468if (!sameClient || client == null)469client = newHttpClient(sameClient);470Thread main = Thread.currentThread();471CompletableFuture<Thread> interruptingThread = new CompletableFuture<>();472Runnable interrupt = () -> {473Thread current = Thread.currentThread();474out.printf("%s Interrupting main from: %s (%s)", now(), current, uri);475interruptingThread.complete(current);476main.interrupt();477};478Iterable<byte[]> iterable = () -> {479var async = random.nextBoolean();480if (async) executor.execute(interrupt);481else interrupt.run();482return List.of(BODY.getBytes(UTF_8)).iterator();483};484485HttpRequest req = HttpRequest.newBuilder(URI.create(uri))486.POST(HttpRequest.BodyPublishers.ofByteArrays(iterable))487.build();488String body = null;489Exception failed = null;490try {491body = client.send(req, BodyHandlers.ofString()).body();492} catch (Exception x) {493failed = x;494}495496if (failed instanceof InterruptedException) {497out.println("Got expected exception: " + failed);498} else if (failed instanceof IOException) {499// that could be OK if the main thread was interrupted500// from the main thread: the interrupt status could have501// been caught by writing to the socket from the main502// thread.503if (interruptingThread.get() == main) {504out.println("Accepting IOException: " + failed);505failed.printStackTrace(out);506} else {507throw failed;508}509} else if (failed != null) {510assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));511throw failed;512}513}514}515516517518@BeforeTest519public void setup() throws Exception {520sslContext = new SimpleSSLContext().get();521if (sslContext == null)522throw new AssertionError("Unexpected null sslContext");523524// HTTP/1.1525HttpTestHandler h1_chunkHandler = new HTTPSlowHandler();526InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);527httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));528httpTestServer.addHandler(h1_chunkHandler, "/http1/x/");529httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/x/";530531HttpsServer httpsServer = HttpsServer.create(sa, 0);532httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));533httpsTestServer = HttpTestServer.of(httpsServer);534httpsTestServer.addHandler(h1_chunkHandler, "/https1/x/");535httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1/x/";536537// HTTP/2538HttpTestHandler h2_chunkedHandler = new HTTPSlowHandler();539540http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));541http2TestServer.addHandler(h2_chunkedHandler, "/http2/x/");542http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x/";543544https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));545https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/");546https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/";547548serverCount.addAndGet(4);549httpTestServer.start();550httpsTestServer.start();551http2TestServer.start();552https2TestServer.start();553}554555@AfterTest556public void teardown() throws Exception {557String sharedClientName =558sharedClient == null ? null : sharedClient.toString();559sharedClient = null;560Thread.sleep(100);561AssertionError fail = TRACKER.check(500);562try {563httpTestServer.stop();564httpsTestServer.stop();565http2TestServer.stop();566https2TestServer.stop();567} finally {568if (fail != null) {569if (sharedClientName != null) {570System.err.println("Shared client name is: " + sharedClientName);571}572throw fail;573}574}575}576577private static boolean isThreadInterrupt(HttpTestExchange t) {578return t.getRequestURI().getPath().contains("/interruptThread");579}580581/**582* A handler that slowly sends back a body to give time for the583* the request to get cancelled before the body is fully received.584*/585static class HTTPSlowHandler implements HttpTestHandler {586@Override587public void handle(HttpTestExchange t) throws IOException {588try {589out.println("HTTPSlowHandler received request to " + t.getRequestURI());590System.err.println("HTTPSlowHandler received request to " + t.getRequestURI());591592boolean isThreadInterrupt = isThreadInterrupt(t);593byte[] req;594try (InputStream is = t.getRequestBody()) {595req = is.readAllBytes();596}597t.sendResponseHeaders(200, -1); // chunked/variable598try (OutputStream os = t.getResponseBody()) {599// lets split the response in several chunks...600String msg = (req != null && req.length != 0)601? new String(req, UTF_8)602: BODY;603String[] str = msg.split("\\|");604for (var s : str) {605req = s.getBytes(UTF_8);606os.write(req);607os.flush();608try {609Thread.sleep(SERVER_LATENCY);610} catch (InterruptedException x) {611// OK612}613out.printf("Server wrote %d bytes%n", req.length);614}615}616} catch (Throwable e) {617out.println("HTTPSlowHandler: unexpected exception: " + e);618e.printStackTrace();619throw e;620} finally {621out.printf("HTTPSlowHandler reply sent: %s%n", t.getRequestURI());622System.err.printf("HTTPSlowHandler reply sent: %s%n", t.getRequestURI());623}624}625}626627}628629630