Path: blob/master/test/jdk/java/net/httpclient/CancelRequestTest.java
66644 views
/*1* Copyright (c) 2020, 2021, 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.ITestResult;48import org.testng.SkipException;49import org.testng.annotations.AfterClass;50import org.testng.annotations.AfterTest;51import org.testng.annotations.BeforeMethod;52import org.testng.annotations.BeforeTest;53import org.testng.annotations.DataProvider;54import org.testng.annotations.Test;5556import javax.net.ssl.SSLContext;57import java.io.IOException;58import java.io.InputStream;59import java.io.OutputStream;60import java.net.InetAddress;61import java.net.InetSocketAddress;62import java.net.URI;63import java.net.http.HttpClient;64import java.net.http.HttpConnectTimeoutException;65import java.net.http.HttpRequest;66import java.net.http.HttpResponse;67import java.net.http.HttpResponse.BodyHandler;68import java.net.http.HttpResponse.BodyHandlers;69import java.util.Arrays;70import java.util.Iterator;71import java.util.List;72import java.util.Random;73import java.util.concurrent.CancellationException;74import java.util.concurrent.CompletableFuture;75import java.util.concurrent.ConcurrentHashMap;76import java.util.concurrent.ConcurrentMap;77import java.util.concurrent.CountDownLatch;78import java.util.concurrent.ExecutionException;79import java.util.concurrent.Executor;80import java.util.concurrent.Executors;81import java.util.concurrent.atomic.AtomicLong;82import java.util.concurrent.atomic.AtomicReference;83import java.util.stream.Collectors;84import java.util.stream.Stream;8586import static java.lang.System.arraycopy;87import static java.lang.System.out;88import static java.nio.charset.StandardCharsets.UTF_8;89import static org.testng.Assert.assertEquals;90import static org.testng.Assert.assertTrue;9192public class CancelRequestTest implements HttpServerAdapters {9394private static final Random random = RandomFactory.getRandom();9596SSLContext sslContext;97HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]98HttpTestServer httpsTestServer; // HTTPS/1.199HttpTestServer http2TestServer; // HTTP/2 ( h2c )100HttpTestServer https2TestServer; // HTTP/2 ( h2 )101String httpURI;102String httpsURI;103String http2URI;104String https2URI;105106static final long SERVER_LATENCY = 75;107static final int MAX_CLIENT_DELAY = 75;108static final int ITERATION_COUNT = 3;109// a shared executor helps reduce the amount of threads created by the test110static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());111static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();112static volatile boolean tasksFailed;113static final AtomicLong serverCount = new AtomicLong();114static final AtomicLong clientCount = new AtomicLong();115static final long start = System.nanoTime();116public static String now() {117long now = System.nanoTime() - start;118long secs = now / 1000_000_000;119long mill = (now % 1000_000_000) / 1000_000;120long nan = now % 1000_000;121return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);122}123124final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;125private volatile HttpClient sharedClient;126127static class TestExecutor implements Executor {128final AtomicLong tasks = new AtomicLong();129Executor executor;130TestExecutor(Executor executor) {131this.executor = executor;132}133134@Override135public void execute(Runnable command) {136long id = tasks.incrementAndGet();137executor.execute(() -> {138try {139command.run();140} catch (Throwable t) {141tasksFailed = true;142System.out.printf(now() + "Task %s failed: %s%n", id, t);143System.err.printf(now() + "Task %s failed: %s%n", id, t);144FAILURES.putIfAbsent("Task " + id, t);145throw t;146}147});148}149}150151protected boolean stopAfterFirstFailure() {152return Boolean.getBoolean("jdk.internal.httpclient.debug");153}154155final AtomicReference<SkipException> skiptests = new AtomicReference<>();156void checkSkip() {157var skip = skiptests.get();158if (skip != null) throw skip;159}160static String name(ITestResult result) {161var params = result.getParameters();162return result.getName()163+ (params == null ? "()" : Arrays.toString(result.getParameters()));164}165166@BeforeMethod167void beforeMethod(ITestContext context) {168if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) {169if (skiptests.get() == null) {170SkipException skip = new SkipException("some tests failed");171skip.setStackTrace(new StackTraceElement[0]);172skiptests.compareAndSet(null, skip);173}174}175}176177@AfterClass178static final void printFailedTests(ITestContext context) {179out.println("\n=========================");180var failed = context.getFailedTests().getAllResults().stream()181.collect(Collectors.toMap(r -> name(r), ITestResult::getThrowable));182FAILURES.putAll(failed);183try {184out.printf("%n%sCreated %d servers and %d clients%n",185now(), serverCount.get(), clientCount.get());186if (FAILURES.isEmpty()) return;187out.println("Failed tests: ");188FAILURES.entrySet().forEach((e) -> {189out.printf("\t%s: %s%n", e.getKey(), e.getValue());190e.getValue().printStackTrace(out);191});192if (tasksFailed) {193System.out.println("WARNING: Some tasks failed");194}195} finally {196out.println("\n=========================\n");197}198}199200private String[] uris() {201return new String[] {202httpURI,203httpsURI,204http2URI,205https2URI,206};207}208209@DataProvider(name = "asyncurls")210public Object[][] asyncurls() {211String[] uris = uris();212Object[][] result = new Object[uris.length * 2 * 3][];213//Object[][] result = new Object[uris.length][];214int i = 0;215for (boolean mayInterrupt : List.of(true, false, true)) {216for (boolean sameClient : List.of(false, true)) {217//if (!sameClient) continue;218for (String uri : uris()) {219String path = sameClient ? "same" : "new";220path = path + (mayInterrupt ? "/interrupt" : "/nointerrupt");221result[i++] = new Object[]{uri + path, sameClient, mayInterrupt};222}223}224}225assert i == uris.length * 2 * 3;226// assert i == uris.length ;227return result;228}229230@DataProvider(name = "urls")231public Object[][] alltests() {232String[] uris = uris();233Object[][] result = new Object[uris.length * 2][];234//Object[][] result = new Object[uris.length][];235int i = 0;236for (boolean sameClient : List.of(false, true)) {237//if (!sameClient) continue;238for (String uri : uris()) {239String path = sameClient ? "same" : "new";240path = path + "/interruptThread";241result[i++] = new Object[]{uri + path, sameClient};242}243}244assert i == uris.length * 2;245// assert i == uris.length ;246return result;247}248249private HttpClient makeNewClient() {250clientCount.incrementAndGet();251return TRACKER.track(HttpClient.newBuilder()252.proxy(HttpClient.Builder.NO_PROXY)253.executor(executor)254.sslContext(sslContext)255.build());256}257258HttpClient newHttpClient(boolean share) {259if (!share) return makeNewClient();260HttpClient shared = sharedClient;261if (shared != null) return shared;262synchronized (this) {263shared = sharedClient;264if (shared == null) {265shared = sharedClient = makeNewClient();266}267return shared;268}269}270271final static String BODY = "Some string | that ? can | be split ? several | ways.";272273// should accept SSLHandshakeException because of the connectionAborter274// with http/2 and should accept Stream 5 cancelled.275// => also examine in what measure we should always276// rewrap in "Request Cancelled" when the multi exchange was aborted...277private static boolean isCancelled(Throwable t) {278while (t instanceof ExecutionException) t = t.getCause();279if (t instanceof CancellationException) return true;280if (t instanceof IOException) return String.valueOf(t).contains("Request cancelled");281out.println("Not a cancellation exception: " + t);282t.printStackTrace(out);283return false;284}285286private static void delay() {287int delay = random.nextInt(MAX_CLIENT_DELAY);288try {289System.out.println("client delay: " + delay);290Thread.sleep(delay);291} catch (InterruptedException x) {292out.println("Unexpected exception: " + x);293}294}295296@Test(dataProvider = "asyncurls")297public void testGetSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning)298throws Exception {299checkSkip();300HttpClient client = null;301uri = uri + "/get";302out.printf("%n%s testGetSendAsync(%s, %b, %b)%n", now(), uri, sameClient, mayInterruptIfRunning);303for (int i=0; i< ITERATION_COUNT; i++) {304if (!sameClient || client == null)305client = newHttpClient(sameClient);306307HttpRequest req = HttpRequest.newBuilder(URI.create(uri))308.GET()309.build();310BodyHandler<String> handler = BodyHandlers.ofString();311CountDownLatch latch = new CountDownLatch(1);312CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);313var cf1 = response.whenComplete((r,t) -> System.out.println(t));314CompletableFuture<HttpResponse<String>> cf2 = cf1.whenComplete((r,t) -> latch.countDown());315out.println("response: " + response);316out.println("cf1: " + cf1);317out.println("cf2: " + cf2);318delay();319cf1.cancel(mayInterruptIfRunning);320out.println("response after cancel: " + response);321out.println("cf1 after cancel: " + cf1);322out.println("cf2 after cancel: " + cf2);323try {324String body = cf2.get().body();325assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));326throw new AssertionError("Expected CancellationException not received");327} catch (ExecutionException x) {328out.println("Got expected exception: " + x);329assertTrue(isCancelled(x));330}331332// Cancelling the request may cause an IOException instead...333boolean hasCancellationException = false;334try {335cf1.get();336} catch (CancellationException | ExecutionException x) {337out.println("Got expected exception: " + x);338assertTrue(isCancelled(x));339hasCancellationException = x instanceof CancellationException;340}341342// because it's cf1 that was cancelled then response might not have343// completed yet - so wait for it here...344try {345String body = response.get().body();346assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));347if (mayInterruptIfRunning) {348// well actually - this could happen... In which case we'll need to349// increase the latency in the server handler...350throw new AssertionError("Expected Exception not received");351}352} catch (ExecutionException x) {353assertEquals(response.isDone(), true);354Throwable wrapped = x.getCause();355assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()));356Throwable cause = wrapped.getCause();357out.println("CancellationException cause: " + x);358assertTrue(IOException.class.isAssignableFrom(cause.getClass()));359if (cause instanceof HttpConnectTimeoutException) {360cause.printStackTrace(out);361throw new RuntimeException("Unexpected timeout exception", cause);362}363if (mayInterruptIfRunning) {364out.println("Got expected exception: " + wrapped);365out.println("\tcause: " + cause);366} else {367out.println("Unexpected exception: " + wrapped);368wrapped.printStackTrace(out);369throw x;370}371}372373assertEquals(response.isDone(), true);374assertEquals(response.isCancelled(), false);375assertEquals(cf1.isCancelled(), hasCancellationException);376assertEquals(cf2.isDone(), true);377assertEquals(cf2.isCancelled(), false);378assertEquals(latch.getCount(), 0);379}380}381382@Test(dataProvider = "asyncurls")383public void testPostSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning)384throws Exception {385checkSkip();386uri = uri + "/post";387HttpClient client = null;388out.printf("%n%s testPostSendAsync(%s, %b, %b)%n", now(), uri, sameClient, mayInterruptIfRunning);389for (int i=0; i< ITERATION_COUNT; i++) {390if (!sameClient || client == null)391client = newHttpClient(sameClient);392393CompletableFuture<CompletableFuture<?>> cancelFuture = new CompletableFuture<>();394395Iterable<byte[]> iterable = new Iterable<byte[]>() {396@Override397public Iterator<byte[]> iterator() {398// this is dangerous399out.println("waiting for completion on: " + cancelFuture);400boolean async = random.nextBoolean();401Runnable cancel = () -> {402out.println("Cancelling from " + Thread.currentThread());403var cf1 = cancelFuture.join();404cf1.cancel(mayInterruptIfRunning);405out.println("cancelled " + cf1);406};407if (async) executor.execute(cancel);408else cancel.run();409return List.of(BODY.getBytes(UTF_8)).iterator();410}411};412413HttpRequest req = HttpRequest.newBuilder(URI.create(uri))414.POST(HttpRequest.BodyPublishers.ofByteArrays(iterable))415.build();416BodyHandler<String> handler = BodyHandlers.ofString();417CountDownLatch latch = new CountDownLatch(1);418CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);419var cf1 = response.whenComplete((r,t) -> System.out.println(t));420CompletableFuture<HttpResponse<String>> cf2 = cf1.whenComplete((r,t) -> latch.countDown());421out.println("response: " + response);422out.println("cf1: " + cf1);423out.println("cf2: " + cf2);424cancelFuture.complete(cf1);425out.println("response after cancel: " + response);426out.println("cf1 after cancel: " + cf1);427out.println("cf2 after cancel: " + cf2);428try {429String body = cf2.get().body();430assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));431throw new AssertionError("Expected CancellationException not received");432} catch (ExecutionException x) {433out.println("Got expected exception: " + x);434assertTrue(isCancelled(x));435}436437// Cancelling the request may cause an IOException instead...438boolean hasCancellationException = false;439try {440cf1.get();441} catch (CancellationException | ExecutionException x) {442out.println("Got expected exception: " + x);443assertTrue(isCancelled(x));444hasCancellationException = x instanceof CancellationException;445}446447// because it's cf1 that was cancelled then response might not have448// completed yet - so wait for it here...449try {450String body = response.get().body();451assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));452if (mayInterruptIfRunning) {453// well actually - this could happen... In which case we'll need to454// increase the latency in the server handler...455throw new AssertionError("Expected Exception not received");456}457} catch (ExecutionException x) {458assertEquals(response.isDone(), true);459Throwable wrapped = x.getCause();460assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()));461Throwable cause = wrapped.getCause();462assertTrue(IOException.class.isAssignableFrom(cause.getClass()));463if (cause instanceof HttpConnectTimeoutException) {464cause.printStackTrace(out);465throw new RuntimeException("Unexpected timeout exception", cause);466}467if (mayInterruptIfRunning) {468out.println("Got expected exception: " + wrapped);469out.println("\tcause: " + cause);470} else {471out.println("Unexpected exception: " + wrapped);472wrapped.printStackTrace(out);473throw x;474}475}476477assertEquals(response.isDone(), true);478assertEquals(response.isCancelled(), false);479assertEquals(cf1.isCancelled(), hasCancellationException);480assertEquals(cf2.isDone(), true);481assertEquals(cf2.isCancelled(), false);482assertEquals(latch.getCount(), 0);483}484}485486@Test(dataProvider = "urls")487public void testPostInterrupt(String uri, boolean sameClient)488throws Exception {489checkSkip();490HttpClient client = null;491out.printf("%n%s testPostInterrupt(%s, %b)%n", now(), uri, sameClient);492for (int i=0; i< ITERATION_COUNT; i++) {493if (!sameClient || client == null)494client = newHttpClient(sameClient);495Thread main = Thread.currentThread();496CompletableFuture<Thread> interruptingThread = new CompletableFuture<>();497Runnable interrupt = () -> {498Thread current = Thread.currentThread();499out.printf("%s Interrupting main from: %s (%s)", now(), current, uri);500interruptingThread.complete(current);501main.interrupt();502};503Iterable<byte[]> iterable = () -> {504var async = random.nextBoolean();505if (async) executor.execute(interrupt);506else interrupt.run();507return List.of(BODY.getBytes(UTF_8)).iterator();508};509510HttpRequest req = HttpRequest.newBuilder(URI.create(uri))511.POST(HttpRequest.BodyPublishers.ofByteArrays(iterable))512.build();513String body = null;514Exception failed = null;515try {516body = client.send(req, BodyHandlers.ofString()).body();517} catch (Exception x) {518failed = x;519}520521if (failed instanceof InterruptedException) {522out.println("Got expected exception: " + failed);523} else if (failed instanceof IOException) {524// that could be OK if the main thread was interrupted525// from the main thread: the interrupt status could have526// been caught by writing to the socket from the main527// thread.528if (interruptingThread.get() == main) {529out.println("Accepting IOException: " + failed);530failed.printStackTrace(out);531} else {532throw failed;533}534} else if (failed != null) {535assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));536throw failed;537}538}539}540541542543@BeforeTest544public void setup() throws Exception {545sslContext = new SimpleSSLContext().get();546if (sslContext == null)547throw new AssertionError("Unexpected null sslContext");548549// HTTP/1.1550HttpTestHandler h1_chunkHandler = new HTTPSlowHandler();551InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);552httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));553httpTestServer.addHandler(h1_chunkHandler, "/http1/x/");554httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/x/";555556HttpsServer httpsServer = HttpsServer.create(sa, 0);557httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));558httpsTestServer = HttpTestServer.of(httpsServer);559httpsTestServer.addHandler(h1_chunkHandler, "/https1/x/");560httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1/x/";561562// HTTP/2563HttpTestHandler h2_chunkedHandler = new HTTPSlowHandler();564565http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));566http2TestServer.addHandler(h2_chunkedHandler, "/http2/x/");567http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x/";568569https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));570https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/");571https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/";572573serverCount.addAndGet(4);574httpTestServer.start();575httpsTestServer.start();576http2TestServer.start();577https2TestServer.start();578}579580@AfterTest581public void teardown() throws Exception {582String sharedClientName =583sharedClient == null ? null : sharedClient.toString();584sharedClient = null;585Thread.sleep(100);586AssertionError fail = TRACKER.check(500);587try {588httpTestServer.stop();589httpsTestServer.stop();590http2TestServer.stop();591https2TestServer.stop();592} finally {593if (fail != null) {594if (sharedClientName != null) {595System.err.println("Shared client name is: " + sharedClientName);596}597throw fail;598}599}600}601602private static boolean isThreadInterrupt(HttpTestExchange t) {603return t.getRequestURI().getPath().contains("/interruptThread");604}605606/**607* A handler that slowly sends back a body to give time for the608* the request to get cancelled before the body is fully received.609*/610static class HTTPSlowHandler implements HttpTestHandler {611@Override612public void handle(HttpTestExchange t) throws IOException {613try {614out.println("HTTPSlowHandler received request to " + t.getRequestURI());615System.err.println("HTTPSlowHandler received request to " + t.getRequestURI());616617boolean isThreadInterrupt = isThreadInterrupt(t);618byte[] req;619try (InputStream is = t.getRequestBody()) {620req = is.readAllBytes();621}622t.sendResponseHeaders(200, -1); // chunked/variable623try (OutputStream os = t.getResponseBody()) {624// lets split the response in several chunks...625String msg = (req != null && req.length != 0)626? new String(req, UTF_8)627: BODY;628String[] str = msg.split("\\|");629for (var s : str) {630req = s.getBytes(UTF_8);631os.write(req);632os.flush();633try {634Thread.sleep(SERVER_LATENCY);635} catch (InterruptedException x) {636// OK637}638out.printf("Server wrote %d bytes%n", req.length);639}640}641} catch (Throwable e) {642out.println("HTTPSlowHandler: unexpected exception: " + e);643e.printStackTrace();644throw e;645} finally {646out.printf("HTTPSlowHandler reply sent: %s%n", t.getRequestURI());647System.err.printf("HTTPSlowHandler reply sent: %s%n", t.getRequestURI());648}649}650}651652}653654655