# HG changeset patch # User chegar # Date 1528293662 -3600 # Node ID 9822bbe48b9bb06f2cabbe9317fb281b5fd868ba # Parent 5bc3d3bb145b0a74868a4d189c30e026683ab108 http-client-branch: more descriptive HTTP/1.1 exception detail messages diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java Wed Jun 06 15:01:02 2018 +0100 @@ -464,8 +464,7 @@ // throw ConnectionExpiredException // to try & force a retry of the request. retry = false; - ex = new ConnectionExpiredException( - "subscription is finished", ex); + ex = new ConnectionExpiredException(ex); } } error = ex; diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java Wed Jun 06 15:01:02 2018 +0100 @@ -45,6 +45,7 @@ import jdk.internal.net.http.common.MinimalFuture; import jdk.internal.net.http.common.Utils; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static jdk.internal.net.http.common.Utils.wrapWithExtraDetail; /** * Encapsulates one HTTP/1.1 request/response exchange. @@ -133,6 +134,9 @@ subscription.request(n); } + /** A current-state message suitable for inclusion in an exception detail message. */ + abstract String currentStateMessage(); + final boolean isSubscribed() { return subscription != null; } @@ -158,6 +162,7 @@ @Override public void onNext(ByteBuffer item) { error(); } @Override public void onError(Throwable throwable) { error(); } @Override public void onComplete() { error(); } + @Override String currentStateMessage() { return null; } private void error() { throw new InternalError("should not reach here"); } @@ -205,8 +210,10 @@ } @Override - public void onReadError(Throwable ex) { - cancelImpl(ex); + public void onReadError(Throwable t) { + if (!bodySentCF.isDone() && bodySubscriber != null) + t = wrapWithExtraDetail(t, bodySubscriber::currentStateMessage); + cancelImpl(t); } @Override diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java Wed Jun 06 15:01:02 2018 +0100 @@ -50,7 +50,8 @@ private HttpHeaders headers; private Map> privateMap = new HashMap<>(); - enum State { STATUS_LINE, + enum State { INITIAL, + STATUS_LINE, STATUS_LINE_FOUND_CR, STATUS_LINE_FOUND_LF, STATUS_LINE_END, @@ -63,7 +64,7 @@ HEADER_FOUND_CR_LF_CR, FINISHED } - private State state = State.STATUS_LINE; + private State state = State.INITIAL; /** Returns the status-line. */ String statusLine() { return statusLine; } @@ -77,6 +78,25 @@ return headers; } + /** A current-state message suitable for inclusion in an exception detail message. */ + public String currentStateMessage() { + String stateName = state.name(); + String msg; + if (stateName.contains("INITIAL")) { + return format("HTTP/1.1 header parser received no bytes"); + } else if (stateName.contains("STATUS")) { + msg = format("parsing HTTP/1.1 status line, receiving [%s]", sb.toString()); + } else if (stateName.contains("HEADER")) { + String headerName = sb.toString(); + if (headerName.indexOf(':') != -1) + headerName = headerName.substring(0, headerName.indexOf(':')+1) + "..."; + msg = format("parsing HTTP/1.1 header, receiving [%s]", headerName); + } else { + msg =format("HTTP/1.1 parser receiving [%s]", state, sb.toString()); + } + return format("%s, parser state [%s]", msg , state); + } + /** * Parses HTTP/1.X status-line and headers from the given bytes. Must be * called successive times, with additional data, until returns true. @@ -92,6 +112,9 @@ while (canContinueParsing(input)) { switch (state) { + case INITIAL: + state = State.STATUS_LINE; + break; case STATUS_LINE: readResumeStatusLine(input); break; diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java Wed Jun 06 15:01:02 2018 +0100 @@ -43,6 +43,7 @@ import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; +import static java.lang.String.format; import static java.nio.charset.StandardCharsets.US_ASCII; /** @@ -347,6 +348,11 @@ } @Override + public String currentStateMessage() { + return "streaming request body " + (complete ? "complete" : "incomplete"); + } + + @Override public void onError(Throwable throwable) { if (complete) return; @@ -414,6 +420,12 @@ } @Override + public String currentStateMessage() { + return format("fixed content-length: %d, bytes sent: %d", + contentLength, contentWritten); + } + + @Override public void onError(Throwable throwable) { if (debug.on()) debug.log("onError"); if (complete) // TODO: error? diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java Wed Jun 06 15:01:02 2018 +0100 @@ -44,9 +44,9 @@ import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.MinimalFuture; import jdk.internal.net.http.common.Utils; - import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpResponse.BodySubscribers.discarding; +import static jdk.internal.net.http.common.Utils.wrapWithExtraDetail; import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; /** @@ -614,6 +614,7 @@ @Override public final void onReadError(Throwable t) { + t = wrapWithExtraDetail(t, parser::currentStateMessage); Http1Response.this.onReadError(t); } @@ -693,6 +694,7 @@ @Override public final void onReadError(Throwable t) { + t = wrapWithExtraDetail(t, parser::currentStateMessage); Http1Response.this.onReadError(t); } diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java Wed Jun 06 15:01:02 2018 +0100 @@ -26,7 +26,6 @@ package jdk.internal.net.http; import java.io.IOException; -import java.lang.System.Logger.Level; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; @@ -34,9 +33,9 @@ import java.util.function.Consumer; import java.net.http.HttpHeaders; import java.net.http.HttpResponse; - import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; +import static java.lang.String.format; /** * Implements chunked/fixed transfer encodings of HTTP/1.1 responses. @@ -95,6 +94,9 @@ interface BodyParser extends Consumer { void onSubscribe(AbstractSubscription sub); + // A current-state message suitable for inclusion in an exception + // detail message. + String currentStateMessage(); } // Returns a parser that will take care of parsing the received byte @@ -145,6 +147,11 @@ } @Override + public String currentStateMessage() { + return format("chunked transfer encoding, state: %s", state); + } + + @Override public void accept(ByteBuffer b) { if (closedExceptionally != null) { if (debug.on()) @@ -425,6 +432,12 @@ } @Override + public String currentStateMessage() { + return format("fixed content-length: %d, bytes received: %d", + contentLength, contentLength - remaining); + } + + @Override public void accept(ByteBuffer b) { if (closedExceptionally != null) { if (debug.on()) diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java Wed Jun 06 15:01:02 2018 +0100 @@ -35,13 +35,12 @@ private static final long serialVersionUID = 0; /** - * Constructs a {@code ConnectionExpiredException} with the specified detail - * message and cause. + * Constructs a {@code ConnectionExpiredException} with a detail message of + * "subscription is finished" and the given cause. * - * @param s the detail message * @param cause the throwable cause */ - public ConnectionExpiredException(String s, Throwable cause) { - super(s, cause); + public ConnectionExpiredException(Throwable cause) { + super("subscription is finished", cause); } } diff -r 5bc3d3bb145b -r 9822bbe48b9b src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java Wed Jun 06 15:52:48 2018 +0100 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java Wed Jun 06 15:01:02 2018 +0100 @@ -34,6 +34,7 @@ import javax.net.ssl.SSLSession; import java.io.ByteArrayOutputStream; import java.io.Closeable; +import java.io.EOFException; import java.io.IOException; import java.io.PrintStream; import java.io.UncheckedIOException; @@ -253,6 +254,35 @@ return new IOException(t); } + /** + * Adds a more specific exception detail message, based on the given + * exception type and the message supplier. This is primarily to present + * more descriptive messages in IOExceptions that may be visible to calling + * code. + * + * @return a possibly new exception that has as its detail message, the + * message from the messageSupplier, and the given throwable as its + * cause. Otherwise returns the given throwable + */ + public static Throwable wrapWithExtraDetail(Throwable t, + Supplier messageSupplier) { + if (!(t instanceof IOException)) + return t; + + String msg = messageSupplier.get(); + if (msg == null) + return t; + + if (t instanceof ConnectionExpiredException) { + IOException ioe = new IOException(msg, t.getCause()); + t = new ConnectionExpiredException(ioe); + } else { + IOException ioe = new IOException(msg, t); + t = ioe; + } + return t; + } + private Utils() { } /** diff -r 5bc3d3bb145b -r 9822bbe48b9b test/jdk/java/net/httpclient/HandshakeFailureTest.java --- a/test/jdk/java/net/httpclient/HandshakeFailureTest.java Wed Jun 06 15:52:48 2018 +0100 +++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java Wed Jun 06 15:01:02 2018 +0100 @@ -92,8 +92,9 @@ HttpResponse response = client.send(request, discarding()); String msg = String.format("UNEXPECTED response=%s%n", response); throw new RuntimeException(msg); - } catch (SSLHandshakeException expected) { + } catch (IOException expected) { out.printf("Client: caught expected exception: %s%n", expected); + checkExceptionOrCause(SSLHandshakeException.class, expected); } } } @@ -111,8 +112,9 @@ HttpResponse response = client.send(request, discarding()); String msg = String.format("UNEXPECTED response=%s%n", response); throw new RuntimeException(msg); - } catch (SSLHandshakeException expected) { + } catch (IOException expected) { out.printf("Client: caught expected exception: %s%n", expected); + checkExceptionOrCause(SSLHandshakeException.class, expected); } } } @@ -132,12 +134,9 @@ String msg = String.format("UNEXPECTED response=%s%n", response); throw new RuntimeException(msg); } catch (CompletionException ce) { - if (ce.getCause() instanceof SSLHandshakeException) { - out.printf("Client: caught expected exception: %s%n", ce.getCause()); - } else { - out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause()); - throw ce; - } + Throwable expected = ce.getCause(); + out.printf("Client: caught expected exception: %s%n", expected); + checkExceptionOrCause(SSLHandshakeException.class, expected); } } } @@ -158,16 +157,25 @@ String msg = String.format("UNEXPECTED response=%s%n", response); throw new RuntimeException(msg); } catch (CompletionException ce) { - if (ce.getCause() instanceof SSLHandshakeException) { - out.printf("Client: caught expected exception: %s%n", ce.getCause()); - } else { - out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause()); - throw ce; - } + ce.printStackTrace(out); + Throwable expected = ce.getCause(); + out.printf("Client: caught expected exception: %s%n", expected); + checkExceptionOrCause(SSLHandshakeException.class, expected); } } } + static void checkExceptionOrCause(Class clazz, Throwable t) { + do { + if (clazz.isInstance(t)) { + System.out.println("Found expected exception/cause: " + t); + return; // found + } + } while ((t = t.getCause()) != null); + t.printStackTrace(System.out); + throw new RuntimeException("Expected " + clazz + "in " + t); + } + /** Common supertype for PlainServer and SSLServer. */ static abstract class AbstractServer extends Thread implements AutoCloseable { protected final ServerSocket ss; diff -r 5bc3d3bb145b -r 9822bbe48b9b test/jdk/java/net/httpclient/InvalidSSLContextTest.java --- a/test/jdk/java/net/httpclient/InvalidSSLContextTest.java Wed Jun 06 15:52:48 2018 +0100 +++ b/test/jdk/java/net/httpclient/InvalidSSLContextTest.java Wed Jun 06 15:01:02 2018 +0100 @@ -53,6 +53,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; @@ -75,6 +77,7 @@ public void testSync(Version version) throws Exception { // client-side uses a different context to that of the server-side HttpClient client = HttpClient.newBuilder() + .proxy(NO_PROXY) .sslContext(SSLContext.getDefault()) .build(); @@ -85,8 +88,9 @@ try { HttpResponse response = client.send(request, BodyHandlers.discarding()); Assert.fail("UNEXPECTED response" + response); - } catch (SSLException sslex) { - System.out.println("Caught expected: " + sslex); + } catch (IOException ex) { + System.out.println("Caught expected: " + ex); + assertExceptionOrCause(SSLException.class, ex); } } @@ -94,6 +98,7 @@ public void testAsync(Version version) throws Exception { // client-side uses a different context to that of the server-side HttpClient client = HttpClient.newBuilder() + .proxy(NO_PROXY) .sslContext(SSLContext.getDefault()) .build(); @@ -117,21 +122,25 @@ if (cause == null) { Assert.fail("Unexpected null cause: " + error); } - assertException(clazz, cause); + assertExceptionOrCause(clazz, cause); } else { - assertException(clazz, error); + assertExceptionOrCause(clazz, error); } return null; }).join(); } - static void assertException(Class clazz, Throwable t) { + static void assertExceptionOrCause(Class clazz, Throwable t) { if (t == null) { Assert.fail("Expected " + clazz + ", caught nothing"); } - if (!clazz.isInstance(t)) { - Assert.fail("Expected " + clazz + ", caught " + t); - } + do { + if (clazz.isInstance(t)) { + return; // found + } + } while ((t = t.getCause()) != null); + t.printStackTrace(System.out); + Assert.fail("Expected " + clazz + "in " + t); } @BeforeTest diff -r 5bc3d3bb145b -r 9822bbe48b9b test/jdk/java/net/httpclient/ShortResponseBody.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/ShortResponseBody.java Wed Jun 06 15:01:02 2018 +0100 @@ -0,0 +1,518 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Tests Exception detail message when too few response bytes are + * received before a socket exception or eof. + * @run testng/othervm ShortResponseBody + * @run testng/othervm -Djdk.httpclient.enableAllMethodRetry ShortResponseBody + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static java.lang.System.out; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.stream.Collectors.toList; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +public class ShortResponseBody { + + Server closeImmediatelyServer; + Server variableLengthServer; + Server fixedLengthServer; + + String httpURIClsImed; + String httpURIVarLen; + String httpURIFixLen; + + static final String EXPECTED_RESPONSE_BODY = + "

Heading

Some Text

"; + + @DataProvider(name = "sanity") + public Object[][] sanity() { + return new Object[][]{ + { httpURIVarLen + "?length=all" }, + { httpURIFixLen + "?length=all" }, + }; + } + + @Test(dataProvider = "sanity") + void sanity(String url) throws Exception { + HttpClient client = HttpClient.newBuilder().build(); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); + HttpResponse response = client.send(request, ofString()); + String body = response.body(); + assertEquals(body, EXPECTED_RESPONSE_BODY); + client.sendAsync(request, ofString()) + .thenApply(resp -> resp.body()) + .thenAccept(b -> assertEquals(b, EXPECTED_RESPONSE_BODY)) + .join(); + } + + @DataProvider(name = "uris") + public Object[][] variants() { + String[][] cases = new String[][] { + // The length query string is the total number of bytes in the reply, + // including headers, before the server closes the connection. The + // second arg is a partial-expected-detail message in the exception. + { httpURIVarLen + "?length=0", "no bytes" }, // EOF without receiving anything + { httpURIVarLen + "?length=1", "status line" }, // EOF during status-line + { httpURIVarLen + "?length=2", "status line" }, + { httpURIVarLen + "?length=10", "status line" }, + { httpURIVarLen + "?length=19", "header" }, // EOF during Content-Type header + { httpURIVarLen + "?length=30", "header" }, + { httpURIVarLen + "?length=45", "header" }, + { httpURIVarLen + "?length=48", "header" }, + { httpURIVarLen + "?length=51", "header" }, + { httpURIVarLen + "?length=98", "header" }, // EOF during Connection header + { httpURIVarLen + "?length=100", "header" }, + { httpURIVarLen + "?length=101", "header" }, + { httpURIVarLen + "?length=104", "header" }, + { httpURIVarLen + "?length=106", "chunked transfer encoding" }, // EOF during chunk header ( length ) + { httpURIVarLen + "?length=110", "chunked transfer encoding" }, // EOF during chunk response body data + + { httpURIFixLen + "?length=0", "no bytes" }, // EOF without receiving anything + { httpURIFixLen + "?length=1", "status line" }, // EOF during status-line + { httpURIFixLen + "?length=2", "status line" }, + { httpURIFixLen + "?length=10", "status line" }, + { httpURIFixLen + "?length=19", "header" }, // EOF during Content-Type header + { httpURIFixLen + "?length=30", "header" }, + { httpURIFixLen + "?length=45", "header" }, + { httpURIFixLen + "?length=48", "header" }, + { httpURIFixLen + "?length=51", "header" }, + { httpURIFixLen + "?length=78", "header" }, // EOF during Connection header + { httpURIFixLen + "?length=79", "header" }, + { httpURIFixLen + "?length=86", "header" }, + { httpURIFixLen + "?length=104", "fixed content-length" }, // EOF during body + { httpURIFixLen + "?length=106", "fixed content-length" }, + { httpURIFixLen + "?length=110", "fixed content-length" }, + + { httpURIClsImed, "no bytes"}, + }; + + List list = new ArrayList<>(); + Arrays.asList(cases).stream() + .map(e -> new Object[] {e[0], e[1], true}) // reuse client + .forEach(list::add); + Arrays.asList(cases).stream() + .map(e -> new Object[] {e[0], e[1], false}) // do not reuse client + .forEach(list::add); + return list.stream().toArray(Object[][]::new); + } + + static final int ITERATION_COUNT = 3; + + @Test(dataProvider = "uris") + void testSynchronousGET(String url, String expectedMsg, boolean sameClient) + throws Exception + { + out.print("---\n"); + HttpClient client = null; + for (int i=0; i< ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); + try { + HttpResponse response = client.send(request, ofString()); + String body = response.body(); + out.println(response + ": " + body); + fail("UNEXPECTED RESPONSE: " + response); + } catch (IOException ioe) { + out.println("Caught expected exception:" + ioe); + String msg = ioe.getMessage(); + assertTrue(msg.contains(expectedMsg), "exception msg:[" + msg + "]"); + // synchronous API must have the send method on the stack + //TODO: uncomment assertSendMethodOnStack(ioe); + assertNoConnectionExpiredException(ioe); + } + } + } + + @Test(dataProvider = "uris") + void testAsynchronousGET(String url, String expectedMsg, boolean sameClient) + throws Exception + { + out.print("---\n"); + HttpClient client = null; + for (int i=0; i< ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); + try { + HttpResponse response = client.sendAsync(request, ofString()).get(); + String body = response.body(); + out.println(response + ": " + body); + fail("UNEXPECTED RESPONSE: " + response); + } catch (ExecutionException ee) { + if (ee.getCause() instanceof IOException) { + IOException ioe = (IOException) ee.getCause(); + out.println("Caught expected exception:" + ioe); + String msg = ioe.getMessage(); + assertTrue(msg.contains(expectedMsg), "exception msg:[" + msg + "]"); + assertNoConnectionExpiredException(ioe); + } else { + throw ee; + } + } + } + } + + // can be used to prolong request body publication + static final class InfiniteInputStream extends InputStream { + @Override + public int read() throws IOException { + return 1; + } + } + + @Test(dataProvider = "uris") + void testSynchronousPOST(String url, String unused, boolean sameClient) + throws Exception + { + out.print("---\n"); + HttpClient client = null; + for (int i=0; i< ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .POST(HttpRequest.BodyPublishers.ofInputStream(() -> new InfiniteInputStream())) + .build(); + try { + HttpResponse response = client.send(request, ofString()); + String body = response.body(); + out.println(response + ": " + body); + fail("UNEXPECTED RESPONSE: " + response); + } catch (IOException ioe) { + out.println("Caught expected exception:" + ioe); + String msg = ioe.getMessage(); + // "incomplete" since the chunked request body is not completely sent + assertTrue(msg.contains("incomplete"), "exception msg:[" + msg + "]"); + // synchronous API must have the send method on the stack + //TODO: uncomment assertSendMethodOnStack(ioe); + assertNoConnectionExpiredException(ioe); + } + } + } + + @Test(dataProvider = "uris") + void testAsynchronousPOST(String url, String unused, boolean sameClient) + throws Exception + { + out.print("---\n"); + HttpClient client = null; + for (int i=0; i< ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .POST(HttpRequest.BodyPublishers.ofInputStream(() -> new InfiniteInputStream())) + .build(); + try { + HttpResponse response = client.sendAsync(request, ofString()).get(); + String body = response.body(); + out.println(response + ": " + body); + fail("UNEXPECTED RESPONSE: " + response); + } catch (ExecutionException ee) { + if (ee.getCause() instanceof IOException) { + IOException ioe = (IOException) ee.getCause(); + out.println("Caught expected exception:" + ioe); + String msg = ioe.getMessage(); + // "incomplete" since the chunked request body is not completely sent + assertTrue(msg.contains("incomplete"), "exception msg:[" + msg + "]"); + assertNoConnectionExpiredException(ioe); + } else { + throw ee; + } + } + } + } + + // Asserts that the "send" method appears in the stack of the given + // exception. The synchronous API must contain the send method on the stack. + static void assertSendMethodOnStack(IOException ioe) { + final String cn = "jdk.internal.net.http.HttpClientImpl"; + List list = Stream.of(ioe.getStackTrace()) + .filter(ste -> ste.getClassName().equals(cn) + && ste.getMethodName().equals("send")) + .collect(toList()); + if (list.size() != 1) { + ioe.printStackTrace(out); + fail(cn + ".send method not found in stack."); + } + } + + // Asserts that the implementation-specific ConnectionExpiredException does + // NOT appear anywhere in the exception or its causal chain. + static void assertNoConnectionExpiredException(IOException ioe) { + Throwable throwable = ioe; + do { + String cn = throwable.getClass().getSimpleName(); + if (cn.equals("ConnectionExpiredException")) { + ioe.printStackTrace(out); + fail("UNEXPECTED ConnectionExpiredException in:[" + ioe + "]"); + } + } while ((throwable = throwable.getCause()) != null); + } + + // -- infra + + /** + * A server that, listens on a port, accepts new connections, and can be + * closed. + */ + static abstract class Server extends Thread implements AutoCloseable { + protected final ServerSocket ss; + protected volatile boolean closed; + + Server(String name) throws IOException { + super(name); + ss = new ServerSocket(); + ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + this.start(); + } + + public int getPort() { return ss.getLocalPort(); } + + @Override + public void close() { + if (closed) + return; + closed = true; + try { + ss.close(); + } catch (IOException e) { + throw new UncheckedIOException("Unexpected", e); + } + } + } + + /** + * A server that closes the connection immediately, without reading or writing. + */ + static final class CloseImmediatelyServer extends Server { + CloseImmediatelyServer() throws IOException { + super("CloseImmediateServer"); + } + + @Override + public void run() { + while (!closed) { + try (Socket s = ss.accept()) { + out.println("Server: got connection, closing immediately "); + } catch (IOException e) { + if (!closed) + throw new UncheckedIOException("Unexpected", e); + } + } + } + } + + /** + * A server that replies with headers and a, possibly partial, reply, before + * closing the connection. The number of bytes of written ( header + body), + * is controllable through the "length" query string param in the requested + * URI. + */ + static abstract class ReplyingServer extends Server { + + private final String name; + + ReplyingServer(String name) throws IOException { + super(name); + this.name = name; + } + + abstract String response(); + + @Override + public void run() { + while (!closed) { + try (Socket s = ss.accept()) { + out.print(name + ": got connection "); + InputStream is = s.getInputStream(); + URI requestMethod = readRequestMethod(is); + out.print(requestMethod + " "); + URI uriPath = readRequestPath(is); + out.println(uriPath); + readRequestHeaders(is); + + String query = uriPath.getRawQuery(); + assert query != null; + String qv = query.split("=")[1]; + int len; + if (qv.equals("all")) { + len = response().getBytes(US_ASCII).length; + } else { + len = Integer.parseInt(query.split("=")[1]); + } + + OutputStream os = s.getOutputStream(); + out.println("Server: writing " + len + " bytes"); + byte[] responseBytes = response().getBytes(US_ASCII); + for (int i = 0; i< len; i++) { + os.write(responseBytes[i]); + } + } catch (IOException e) { + if (!closed) + throw new UncheckedIOException("Unexpected", e); + } + } + } + + static final byte[] requestEnd = new byte[] { '\r', '\n', '\r', '\n' }; + + // Read the request method + static URI readRequestMethod(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + int r; + while ((r = is.read()) != -1 && r != 0x20) { + sb.append((char)r); + } + return URI.create(sb.toString()); + } + + // Read the request URI path + static URI readRequestPath(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + int r; + while ((r = is.read()) != -1 && r != 0x20) { + sb.append((char)r); + } + return URI.create(sb.toString()); + } + + // Read until the end of a HTTP request headers + static void readRequestHeaders(InputStream is) throws IOException { + int requestEndCount = 0, r; + while ((r = is.read()) != -1) { + if (r == requestEnd[requestEndCount]) { + requestEndCount++; + if (requestEndCount == 4) { + break; + } + } else { + requestEndCount = 0; + } + } + } + } + + /** A server that issues a chunked reply. */ + static final class VariableLengthServer extends ReplyingServer { + + static final String CHUNKED_RESPONSE_BODY = + "6\r\n"+ "\r\n" + + "6\r\n"+ "\r\n" + + "10\r\n"+ "

Heading

\r\n" + + "10\r\n"+ "

Some Text

\r\n" + + "7\r\n"+ "\r\n" + + "7\r\n"+ "\r\n" + + "0\r\n"+ "\r\n"; + + static final String RESPONSE_HEADERS = + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Connection: close\r\n\r\n"; + + static final String RESPONSE = RESPONSE_HEADERS + CHUNKED_RESPONSE_BODY; + + VariableLengthServer() throws IOException { + super("VariableLengthServer"); + } + + @Override + String response( ) { return RESPONSE; } + } + + /** A server that issues a fixed-length reply. */ + static final class FixedLengthServer extends ReplyingServer { + + static final String RESPONSE_BODY = EXPECTED_RESPONSE_BODY; + + static final String RESPONSE_HEADERS = + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Content-Length: " + RESPONSE_BODY.length() + "\r\n" + + "Connection: close\r\n\r\n"; + + static final String RESPONSE = RESPONSE_HEADERS + RESPONSE_BODY; + + FixedLengthServer() throws IOException { + super("FixedLengthServer"); + } + + @Override + String response( ) { return RESPONSE; } + } + + static String serverAuthority(Server server) { + return InetAddress.getLoopbackAddress().getHostName() + ":" + + server.getPort(); + } + + @BeforeTest + public void setup() throws Exception { + closeImmediatelyServer = new CloseImmediatelyServer(); + httpURIClsImed = "http://" + serverAuthority(closeImmediatelyServer) + + "/http1/closeImmediately/foo"; + + variableLengthServer = new VariableLengthServer(); + httpURIVarLen = "http://" + serverAuthority(variableLengthServer) + + "/http1/variable/bar"; + + fixedLengthServer = new FixedLengthServer(); + httpURIFixLen = "http://" + serverAuthority(fixedLengthServer) + + "/http1/fixed/baz"; + } + + @AfterTest + public void teardown() throws Exception { + closeImmediatelyServer.close(); + variableLengthServer.close(); + fixedLengthServer.close(); + } +} \ No newline at end of file diff -r 5bc3d3bb145b -r 9822bbe48b9b test/jdk/java/net/httpclient/ssltest/CertificateTest.java --- a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java Wed Jun 06 15:52:48 2018 +0100 +++ b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java Wed Jun 06 15:01:02 2018 +0100 @@ -22,6 +22,7 @@ */ import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpResponse.BodyHandlers; @@ -30,6 +31,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; +import static java.net.http.HttpClient.Builder.NO_PROXY; /* * @test @@ -112,6 +114,7 @@ Exception exception = null; System.out.println("Making request to " + uri_s); HttpClient client = HttpClient.newBuilder() + .proxy(NO_PROXY) .sslContext(ctx) .sslParameters(params) .build(); @@ -128,7 +131,9 @@ error = "Test failed: good: status should be 200"; else if (!expectSuccess) error = "Test failed: bad: status should not be 200"; - } catch (SSLException e) { + } catch (IOException e) { + // there must be an SSLException as the exception or cause + checkExceptionOrCause(SSLException.class, e); System.err.println("Caught Exception " + e + ". expectSuccess = " + expectSuccess); exception = e; if (expectSuccess) @@ -137,4 +142,15 @@ if (error != null) throw new RuntimeException(error, exception); } + + static void checkExceptionOrCause(Class clazz, Throwable t) { + do { + if (clazz.isInstance(t)) { + System.out.println("Found expected exception/cause: " + t); + return; // found + } + } while ((t = t.getCause()) != null); + t.printStackTrace(System.out); + throw new RuntimeException("Expected " + clazz + "in " + t); + } }