8208391: Differentiate response and connect timeouts in HTTP Client API
authorchegar
Thu, 09 Aug 2018 11:23:12 +0100
changeset 51364 31d9e82b2e64
parent 51363 a6fa2016cff1
child 51365 1ba7c83cd8a6
8208391: Differentiate response and connect timeouts in HTTP Client API Reviewed-by: michaelm
src/java.net.http/share/classes/java/net/http/HttpClient.java
src/java.net.http/share/classes/java/net/http/HttpConnectTimeoutException.java
src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java
src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpClientFacade.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/ResponseTimerEvent.java
src/java.net.http/share/classes/jdk/internal/net/http/TimeoutEvent.java
test/jdk/java/net/httpclient/AbstractConnectTimeout.java
test/jdk/java/net/httpclient/AbstractConnectTimeoutHandshake.java
test/jdk/java/net/httpclient/ConnectTimeoutHandshakeAsync.java
test/jdk/java/net/httpclient/ConnectTimeoutHandshakeSync.java
test/jdk/java/net/httpclient/ConnectTimeoutNoProxyAsync.java
test/jdk/java/net/httpclient/ConnectTimeoutNoProxySync.java
test/jdk/java/net/httpclient/ConnectTimeoutWithProxyAsync.java
test/jdk/java/net/httpclient/ConnectTimeoutWithProxySync.java
test/jdk/java/net/httpclient/HttpClientBuilderTest.java
test/jdk/java/net/httpclient/TimeoutBasic.java
test/jdk/java/net/httpclient/examples/JavadocExamples.java
test/jdk/java/net/httpclient/offline/DelegatingHttpClient.java
test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java
--- a/src/java.net.http/share/classes/java/net/http/HttpClient.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java	Thu Aug 09 11:23:12 2018 +0100
@@ -34,6 +34,7 @@
 import java.net.URLPermission;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
@@ -84,6 +85,7 @@
  * <pre>{@code    HttpClient client = HttpClient.newBuilder()
  *        .version(Version.HTTP_1_1)
  *        .followRedirects(Redirect.NORMAL)
+ *        .connectTimeout(Duration.ofSeconds(20))
  *        .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
  *        .authenticator(Authenticator.getDefault())
  *        .build();
@@ -94,7 +96,7 @@
  * <p><b>Asynchronous Example</b>
  * <pre>{@code    HttpRequest request = HttpRequest.newBuilder()
  *        .uri(URI.create("https://foo.com/"))
- *        .timeout(Duration.ofMinutes(1))
+ *        .timeout(Duration.ofMinutes(2))
  *        .header("Content-Type", "application/json")
  *        .POST(BodyPublishers.ofFile(Paths.get("file.json")))
  *        .build();
@@ -197,6 +199,26 @@
         public Builder cookieHandler(CookieHandler cookieHandler);
 
         /**
+         * Sets the connect timeout duration for this client.
+         *
+         * <p> In the case where a new connection needs to be established, if
+         * the connection cannot be established within the given {@code
+         * duration}, then {@link HttpClient#send(HttpRequest,BodyHandler)
+         * HttpClient::send} throws an {@link HttpConnectTimeoutException}, or
+         * {@link HttpClient#sendAsync(HttpRequest,BodyHandler)
+         * HttpClient::sendAsync} completes exceptionally with an
+         * {@code HttpConnectTimeoutException}. If a new connection does not
+         * need to be established, for example if a connection can be reused
+         * from a previous request, then this timeout duration has no effect.
+         *
+         * @param duration the duration to allow the underlying connection to be
+         *                 established
+         * @return this builder
+         * @throws IllegalArgumentException if the duration is non-positive
+         */
+        public Builder connectTimeout(Duration duration);
+
+        /**
          * Sets an {@code SSLContext}.
          *
          * <p> If this method is not invoked prior to {@linkplain #build()
@@ -345,6 +367,17 @@
     public abstract Optional<CookieHandler> cookieHandler();
 
     /**
+     * Returns an {@code Optional} containing the <i>connect timeout duration</i>
+     * for this client. If the {@linkplain Builder#connectTimeout(Duration)
+     * connect timeout duration} was not set in the client's builder, then the
+     * {@code Optional} is empty.
+     *
+     * @return an {@code Optional} containing this client's connect timeout
+     *         duration
+     */
+     public abstract Optional<Duration> connectTimeout();
+
+    /**
      * Returns the follow redirects policy for this client. The default value
      * for client's built by builders that do not specify a redirect policy is
      * {@link HttpClient.Redirect#NEVER NEVER}.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/java/net/http/HttpConnectTimeoutException.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,48 @@
+/*
+ * 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.
+ */
+
+package java.net.http;
+
+/**
+ * Thrown when a connection, over which an {@code HttpRequest} is intended to be
+ * sent, is not successfully established within a specified time period.
+ *
+ * @since 11
+ */
+public class HttpConnectTimeoutException extends HttpTimeoutException {
+
+    private static final long serialVersionUID = 321L + 11L;
+
+    /**
+     * Constructs an {@code HttpConnectTimeoutException} with the given detail
+     * message.
+     *
+     * @param message
+     *        The detail message; can be {@code null}
+     */
+    public HttpConnectTimeoutException(String message) {
+        super(message);
+    }
+}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -80,11 +80,9 @@
         engine = createEngine(context, serverName.getName(), port, sslParameters);
     }
 
-    abstract HttpConnection plainConnection();
     abstract SSLTube getConnectionFlow();
 
     final CompletableFuture<String> getALPN() {
-        assert connected();
         return getConnectionFlow().getALPN();
     }
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -28,6 +28,8 @@
 import java.net.InetSocketAddress;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import jdk.internal.net.http.common.MinimalFuture;
 import jdk.internal.net.http.common.SSLTube;
 import jdk.internal.net.http.common.Utils;
 
@@ -49,14 +51,9 @@
     }
 
     @Override
-    PlainHttpConnection plainConnection() {
-        return plainConnection;
-    }
-
-    @Override
-    public CompletableFuture<Void> connectAsync() {
+    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
         return plainConnection
-                .connectAsync()
+                .connectAsync(exchange)
                 .thenApply( unused -> {
                     // create the SSLTube wrapping the SocketTube, with the given engine
                     flow = new SSLTube(engine,
@@ -67,6 +64,21 @@
     }
 
     @Override
+    public CompletableFuture<Void> finishConnect() {
+        // The actual ALPN value, which may be the empty string, is not
+        // interesting at this point, only that the handshake has completed.
+        return getALPN()
+                .handle((String unused, Throwable ex) -> {
+                    if (ex == null) {
+                        return plainConnection.finishConnect();
+                    } else {
+                        plainConnection.close();
+                        return MinimalFuture.<Void>failedFuture(ex);
+                    } })
+                .thenCompose(Function.identity());
+    }
+
+    @Override
     boolean connected() {
         return plainConnection.connected();
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -29,6 +29,8 @@
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
 import java.net.http.HttpHeaders;
+import java.util.function.Function;
+import jdk.internal.net.http.common.MinimalFuture;
 import jdk.internal.net.http.common.SSLTube;
 import jdk.internal.net.http.common.Utils;
 
@@ -53,13 +55,13 @@
     }
 
     @Override
-    public CompletableFuture<Void> connectAsync() {
+    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
         if (debug.on()) debug.log("Connecting plain tunnel connection");
         // This will connect the PlainHttpConnection flow, so that
         // its HttpSubscriber and HttpPublisher are subscribed to the
         // SocketTube
         return plainConnection
-                .connectAsync()
+                .connectAsync(exchange)
                 .thenApply( unused -> {
                     if (debug.on()) debug.log("creating SSLTube");
                     // create the SSLTube wrapping the SocketTube, with the given engine
@@ -71,6 +73,21 @@
     }
 
     @Override
+    public CompletableFuture<Void> finishConnect() {
+        // The actual ALPN value, which may be the empty string, is not
+        // interesting at this point, only that the handshake has completed.
+        return getALPN()
+                .handle((String unused, Throwable ex) -> {
+                    if (ex == null) {
+                        return plainConnection.finishConnect();
+                    } else {
+                        plainConnection.close();
+                        return MinimalFuture.<Void>failedFuture(ex);
+                    } })
+                .thenCompose(Function.identity());
+    }
+
+    @Override
     boolean isTunnel() { return true; }
 
     @Override
@@ -87,11 +104,6 @@
     }
 
     @Override
-    PlainTunnelingConnection plainConnection() {
-        return plainConnection;
-    }
-
-    @Override
     ConnectionPool.CacheKey cacheKey() {
         return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java	Thu Aug 09 11:23:12 2018 +0100
@@ -83,6 +83,10 @@
     final PushGroup<T> pushGroup;
     final String dbgTag;
 
+    // Keeps track of the underlying connection when establishing an HTTP/2
+    // exchange so that it can be aborted/timed out mid setup.
+    final ConnectionAborter connectionAborter = new ConnectionAborter();
+
     Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
         this.request = request;
         this.upgrading = false;
@@ -125,6 +129,27 @@
         return client;
     }
 
+    // Keeps track of the underlying connection when establishing an HTTP/2
+    // exchange so that it can be aborted/timed out mid setup.
+    static final class ConnectionAborter {
+        private volatile HttpConnection connection;
+
+        void connection(HttpConnection connection) {
+            this.connection = connection;
+        }
+
+        void closeConnection() {
+            HttpConnection connection = this.connection;
+            this.connection = null;
+            if (connection != null) {
+                try {
+                    connection.close();
+                } catch (Throwable t) {
+                    // ignore
+                }
+            }
+        }
+    }
 
     public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
         // If we received a 407 while establishing the exchange
@@ -179,6 +204,7 @@
     }
 
     public void cancel(IOException cause) {
+        if (debug.on()) debug.log("cancel exchImpl: %s, with \"%s\"", exchImpl, cause);
         // If the impl is non null, propagate the exception right away.
         // Otherwise record it so that it can be propagated once the
         // exchange impl has been established.
@@ -190,6 +216,11 @@
         } else {
             // no impl yet. record the exception
             failed = cause;
+
+            // abort/close the connection if setting up the exchange. This can
+            // be important when setting up HTTP/2
+            connectionAborter.closeConnection();
+
             // now call checkCancelled to recheck the impl.
             // if the failed state is set and the impl is not null, reset
             // the failed state and propagate the exception to the impl.
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java	Thu Aug 09 11:23:12 2018 +0100
@@ -85,7 +85,7 @@
         } else {
             Http2ClientImpl c2 = exchange.client().client2(); // #### improve
             HttpRequestImpl request = exchange.request();
-            CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
+            CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request, exchange);
             if (debug.on())
                 debug.log("get: Trying to get HTTP/2 connection");
             return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java	Thu Aug 09 11:23:12 2018 +0100
@@ -233,7 +233,8 @@
         CompletableFuture<Void> connectCF;
         if (!connection.connected()) {
             if (debug.on()) debug.log("initiating connect async");
-            connectCF = connection.connectAsync();
+            connectCF = connection.connectAsync(exchange)
+                    .thenCompose(unused -> connection.finishConnect());
             Throwable cancelled;
             synchronized (lock) {
                 if ((cancelled = failed) == null) {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java	Thu Aug 09 11:23:12 2018 +0100
@@ -90,7 +90,8 @@
      * 3. completes normally with null: no connection in cache for h2c or h2 failed previously
      * 4. completes normally with connection: h2 or h2c connection in cache. Use it.
      */
-    CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
+    CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
+                                                        Exchange<?> exchange) {
         URI uri = req.uri();
         InetSocketAddress proxy = req.proxy();
         String key = Http2Connection.keyFor(uri, proxy);
@@ -123,7 +124,7 @@
             }
         }
         return Http2Connection
-                .createAsync(req, this)
+                .createAsync(req, this, exchange)
                 .whenComplete((conn, t) -> {
                     synchronized (Http2ClientImpl.this) {
                         if (conn != null) {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -353,7 +353,8 @@
 
     // Requires TLS handshake. So, is really async
     static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
-                                                          Http2ClientImpl h2client) {
+                                                          Http2ClientImpl h2client,
+                                                          Exchange<?> exchange) {
         assert request.secure();
         AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
         HttpConnection.getConnection(request.getAddress(),
@@ -361,7 +362,12 @@
                                      request,
                                      HttpClient.Version.HTTP_2);
 
-        return connection.connectAsync()
+        // Expose the underlying connection to the exchange's aborter so it can
+        // be closed if a timeout occurs.
+        exchange.connectionAborter.connection(connection);
+
+        return connection.connectAsync(exchange)
+                  .thenCompose(unused -> connection.finishConnect())
                   .thenCompose(unused -> checkSSLConfig(connection))
                   .thenCompose(notused-> {
                       CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java	Thu Aug 09 11:23:12 2018 +0100
@@ -28,6 +28,7 @@
 import java.net.Authenticator;
 import java.net.CookieHandler;
 import java.net.ProxySelector;
+import java.time.Duration;
 import java.util.concurrent.Executor;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLParameters;
@@ -38,6 +39,7 @@
 public class HttpClientBuilderImpl implements HttpClient.Builder {
 
     CookieHandler cookieHandler;
+    Duration connectTimeout;
     HttpClient.Redirect followRedirects;
     ProxySelector proxy;
     Authenticator authenticator;
@@ -55,6 +57,14 @@
         return this;
     }
 
+    @Override
+    public HttpClientBuilderImpl connectTimeout(Duration duration) {
+        requireNonNull(duration);
+        if (duration.isNegative() || Duration.ZERO.equals(duration))
+            throw new IllegalArgumentException("Invalid duration: " + duration);
+        this.connectTimeout = duration;
+        return this;
+    }
 
     @Override
     public HttpClientBuilderImpl sslContext(SSLContext sslContext) {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientFacade.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientFacade.java	Thu Aug 09 11:23:12 2018 +0100
@@ -30,6 +30,7 @@
 import java.net.Authenticator;
 import java.net.CookieHandler;
 import java.net.ProxySelector;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
@@ -70,6 +71,11 @@
     }
 
     @Override
+    public Optional<Duration> connectTimeout() {
+        return impl.connectTimeout();
+    }
+
+    @Override
     public Redirect followRedirects() {
         return impl.followRedirects();
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java	Thu Aug 09 11:23:12 2018 +0100
@@ -35,6 +35,7 @@
 import java.net.ConnectException;
 import java.net.CookieHandler;
 import java.net.ProxySelector;
+import java.net.http.HttpConnectTimeoutException;
 import java.net.http.HttpTimeoutException;
 import java.nio.ByteBuffer;
 import java.nio.channels.CancelledKeyException;
@@ -47,6 +48,7 @@
 import java.security.AccessController;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivilegedAction;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -154,6 +156,7 @@
     }
 
     private final CookieHandler cookieHandler;
+    private final Duration connectTimeout;
     private final Redirect followRedirects;
     private final Optional<ProxySelector> userProxySelector;
     private final ProxySelector proxySelector;
@@ -278,6 +281,7 @@
         facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
         client2 = new Http2ClientImpl(this);
         cookieHandler = builder.cookieHandler;
+        connectTimeout = builder.connectTimeout;
         followRedirects = builder.followRedirects == null ?
                 Redirect.NEVER : builder.followRedirects;
         this.userProxySelector = Optional.ofNullable(builder.proxy);
@@ -547,6 +551,10 @@
                 throw new IllegalArgumentException(msg, throwable);
             } else if (throwable instanceof SecurityException) {
                 throw new SecurityException(msg, throwable);
+            } else if (throwable instanceof HttpConnectTimeoutException) {
+                HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg);
+                hcte.initCause(throwable);
+                throw hcte;
             } else if (throwable instanceof HttpTimeoutException) {
                 throw new HttpTimeoutException(msg);
             } else if (throwable instanceof ConnectException) {
@@ -1124,6 +1132,11 @@
     }
 
     @Override
+    public Optional<Duration> connectTimeout() {
+        return Optional.ofNullable(connectTimeout);
+    }
+
+    @Override
     public Optional<ProxySelector> proxy() {
         return this.userProxySelector;
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -108,9 +108,20 @@
         return client;
     }
 
-    //public abstract void connect() throws IOException, InterruptedException;
+    /**
+     * Initiates the connect phase.
+     *
+     * Returns a CompletableFuture that completes when the underlying
+     * TCP connection has been established or an error occurs.
+     */
+    public abstract CompletableFuture<Void> connectAsync(Exchange<?> exchange);
 
-    public abstract CompletableFuture<Void> connectAsync();
+    /**
+     * Finishes the connection phase.
+     *
+     * Returns a CompletableFuture that completes when any additional,
+     * type specific, setup has been done. Must be called after connectAsync. */
+    public abstract CompletableFuture<Void> finishConnect();
 
     /** Tells whether, or not, this connection is connected to its destination. */
     abstract boolean connected();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java	Thu Aug 09 11:23:12 2018 +0100
@@ -27,7 +27,7 @@
 
 import java.io.IOException;
 import java.net.ConnectException;
-import java.time.Duration;
+import java.net.http.HttpConnectTimeoutException;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.security.AccessControlContext;
@@ -88,7 +88,7 @@
     );
 
     private final LinkedList<HeaderFilter> filters;
-    TimedEvent timedEvent;
+    ResponseTimerEvent responseTimerEvent;
     volatile boolean cancelled;
     final PushGroup<T> pushGroup;
 
@@ -134,7 +134,7 @@
         this.exchange = new Exchange<>(request, this);
     }
 
-    private synchronized Exchange<T> getExchange() {
+    synchronized Exchange<T> getExchange() {
         return exchange;
     }
 
@@ -157,8 +157,8 @@
     }
 
     private void cancelTimer() {
-        if (timedEvent != null) {
-            client.cancelTimer(timedEvent);
+        if (responseTimerEvent != null) {
+            client.cancelTimer(responseTimerEvent);
         }
     }
 
@@ -220,8 +220,8 @@
             cf = failedFuture(new IOException("Too many retries", retryCause));
         } else {
             if (currentreq.timeout().isPresent()) {
-                timedEvent = new TimedEvent(currentreq.timeout().get());
-                client.registerTimer(timedEvent);
+                responseTimerEvent = ResponseTimerEvent.of(this);
+                client.registerTimer(responseTimerEvent);
             }
             try {
                 // 1. apply request filters
@@ -344,7 +344,9 @@
             }
         }
         if (cancelled && t instanceof IOException) {
-            t = new HttpTimeoutException("request timed out");
+            if (!(t instanceof HttpTimeoutException)) {
+                t = toTimeoutException((IOException)t);
+            }
         } else if (retryOnFailure(t)) {
             Throwable cause = retryCause(t);
 
@@ -378,17 +380,24 @@
         return failedFuture(t);
     }
 
-    class TimedEvent extends TimeoutEvent {
-        TimedEvent(Duration duration) {
-            super(duration);
+    private HttpTimeoutException toTimeoutException(IOException ioe) {
+        HttpTimeoutException t = null;
+
+        // more specific, "request timed out", when connected
+        Exchange<?> exchange = getExchange();
+        if (exchange != null) {
+            ExchangeImpl<?> exchangeImpl = exchange.exchImpl;
+            if (exchangeImpl != null) {
+                if (exchangeImpl.connection().connected()) {
+                    t = new HttpTimeoutException("request timed out");
+                    t.initCause(ioe);
+                }
+            }
         }
-        @Override
-        public void handle() {
-            if (debug.on()) {
-                debug.log("Cancelling MultiExchange due to timeout for request %s",
-                        request);
-            }
-            cancel(new HttpTimeoutException("request timed out"));
+        if (t == null) {
+            t = new HttpConnectTimeoutException("HTTP connect timed out");
+            t.initCause(new ConnectException("HTTP connect timed out"));
         }
+        return t;
     }
 }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -26,6 +26,7 @@
 package jdk.internal.net.http;
 
 import java.io.IOException;
+import java.net.ConnectException;
 import java.net.InetSocketAddress;
 import java.net.StandardSocketOptions;
 import java.nio.channels.SelectableChannel;
@@ -34,6 +35,7 @@
 import java.security.AccessController;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
 import jdk.internal.net.http.common.FlowTube;
 import jdk.internal.net.http.common.Log;
@@ -53,9 +55,52 @@
     private final PlainHttpPublisher writePublisher = new PlainHttpPublisher(reading);
     private volatile boolean connected;
     private boolean closed;
+    private volatile ConnectTimerEvent connectTimerEvent;  // may be null
 
     // should be volatile to provide proper synchronization(visibility) action
 
+    /**
+     * Returns a ConnectTimerEvent iff there is a connect timeout duration,
+     * otherwise null.
+     */
+    private ConnectTimerEvent newConnectTimer(Exchange<?> exchange,
+                                              CompletableFuture<Void> cf) {
+        Duration duration = client().connectTimeout().orElse(null);
+        if (duration != null) {
+            ConnectTimerEvent cte = new ConnectTimerEvent(duration, exchange, cf);
+            return cte;
+        }
+        return null;
+    }
+
+    final class ConnectTimerEvent extends TimeoutEvent {
+        private final CompletableFuture<Void> cf;
+        private final Exchange<?> exchange;
+
+        ConnectTimerEvent(Duration duration,
+                          Exchange<?> exchange,
+                          CompletableFuture<Void> cf) {
+            super(duration);
+            this.exchange = exchange;
+            this.cf = cf;
+        }
+
+        @Override
+        public void handle() {
+            if (debug.on()) {
+                debug.log("HTTP connect timed out");
+            }
+            ConnectException ce = new ConnectException("HTTP connect timed out");
+            exchange.multi.cancel(ce);
+            client().theExecutor().execute(() -> cf.completeExceptionally(ce));
+        }
+
+        @Override
+        public String toString() {
+            return "ConnectTimerEvent, " + super.toString();
+        }
+    }
+
     final class ConnectEvent extends AsyncEvent {
         private final CompletableFuture<Void> cf;
 
@@ -85,7 +130,6 @@
                 if (debug.on())
                     debug.log("ConnectEvent: connect finished: %s Local addr: %s",
                               finished, chan.getLocalAddress());
-                connected = true;
                 // complete async since the event runs on the SelectorManager thread
                 cf.completeAsync(() -> null, client().theExecutor());
             } catch (Throwable e) {
@@ -103,12 +147,20 @@
     }
 
     @Override
-    public CompletableFuture<Void> connectAsync() {
+    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
         CompletableFuture<Void> cf = new MinimalFuture<>();
         try {
             assert !connected : "Already connected";
             assert !chan.isBlocking() : "Unexpected blocking channel";
-            boolean finished = false;
+            boolean finished;
+
+            connectTimerEvent = newConnectTimer(exchange, cf);
+            if (connectTimerEvent != null) {
+                if (debug.on())
+                    debug.log("registering connect timer: " + connectTimerEvent);
+                client().registerTimer(connectTimerEvent);
+            }
+
             PrivilegedExceptionAction<Boolean> pa =
                     () -> chan.connect(Utils.resolveAddress(address));
             try {
@@ -118,7 +170,6 @@
             }
             if (finished) {
                 if (debug.on()) debug.log("connect finished without blocking");
-                connected = true;
                 cf.complete(null);
             } else {
                 if (debug.on()) debug.log("registering connect event");
@@ -137,6 +188,16 @@
     }
 
     @Override
+    public CompletableFuture<Void> finishConnect() {
+        assert connected == false;
+        if (debug.on()) debug.log("finishConnect, setting connected=true");
+        connected = true;
+        if (connectTimerEvent != null)
+            client().cancelTimer(connectTimerEvent);
+        return MinimalFuture.completedFuture(null);
+    }
+
+    @Override
     SocketChannel channel() {
         return chan;
     }
@@ -210,6 +271,8 @@
             Log.logTrace("Closing: " + toString());
             if (debug.on())
                 debug.log("Closing channel: " + client().debugInterestOps(chan));
+            if (connectTimerEvent != null)
+                client().cancelTimer(connectTimerEvent);
             chan.close();
             tube.signalClosed();
         } catch (IOException e) {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java	Thu Aug 09 11:23:12 2018 +0100
@@ -26,11 +26,13 @@
 package jdk.internal.net.http;
 
 import java.io.IOException;
-import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
+import java.net.http.HttpTimeoutException;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
+import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.function.Function;
 import java.net.http.HttpHeaders;
 import jdk.internal.net.http.common.FlowTube;
@@ -60,9 +62,10 @@
     }
 
     @Override
-    public CompletableFuture<Void> connectAsync() {
+    public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
         if (debug.on()) debug.log("Connecting plain connection");
-        return delegate.connectAsync()
+        return delegate.connectAsync(exchange)
+            .thenCompose(unused -> delegate.finishConnect())
             .thenCompose((Void v) -> {
                 if (debug.on()) debug.log("sending HTTP/1.1 CONNECT");
                 HttpClientImpl client = client();
@@ -70,7 +73,7 @@
                 HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
                 MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
                         client, discarding(), null, null);
-                Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
+                Exchange<Void> connectExchange = mulEx.getExchange();
 
                 return connectExchange
                         .responseAsyncImpl(delegate)
@@ -96,14 +99,36 @@
                                 ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).drainLeftOverBytes();
                                 int remaining = b.remaining();
                                 assert remaining == 0: "Unexpected remaining: " + remaining;
-                                connected = true;
                                 cf.complete(null);
                             }
                             return cf;
-                        });
+                        })
+                        .handle((result, ex) -> {
+                            if (ex == null) {
+                                return MinimalFuture.completedFuture(result);
+                            } else {
+                                if (debug.on())
+                                    debug.log("tunnel failed with \"%s\"", ex.toString());
+                                Throwable t = ex;
+                                if (t instanceof CompletionException)
+                                    t = t.getCause();
+                                if (t instanceof HttpTimeoutException) {
+                                    String msg = "proxy tunneling CONNECT request timed out";
+                                    t = new HttpTimeoutException(msg);
+                                    t.initCause(ex);
+                                }
+                                return MinimalFuture.<Void>failedFuture(t);
+                            }
+                        })
+                        .thenCompose(Function.identity());
             });
     }
 
+    public CompletableFuture<Void> finishConnect() {
+        connected = true;
+        return MinimalFuture.completedFuture(null);
+    }
+
     @Override
     boolean isTunnel() { return true; }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseTimerEvent.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,78 @@
+/*
+ * 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.
+ */
+
+package jdk.internal.net.http;
+
+import java.net.ConnectException;
+import java.net.http.HttpConnectTimeoutException;
+import java.net.http.HttpTimeoutException;
+import jdk.internal.net.http.common.Logger;
+import jdk.internal.net.http.common.Utils;
+
+public class ResponseTimerEvent extends TimeoutEvent {
+    private static final Logger debug =
+            Utils.getDebugLogger("ResponseTimerEvent"::toString, Utils.DEBUG);
+
+    private final MultiExchange<?> multiExchange;
+
+    static ResponseTimerEvent of(MultiExchange<?> exchange) {
+        return new ResponseTimerEvent(exchange);
+    }
+
+    private ResponseTimerEvent(MultiExchange<?> multiExchange) {
+        super(multiExchange.exchange.request.timeout().get());
+        this.multiExchange = multiExchange;
+    }
+
+    @Override
+    public void handle() {
+        if (debug.on()) {
+            debug.log("Cancelling MultiExchange due to timeout for request %s",
+                      multiExchange.exchange.request);
+        }
+        HttpTimeoutException t = null;
+
+        // more specific, "request timed out", message when connected
+        Exchange<?> exchange = multiExchange.getExchange();
+        if (exchange != null) {
+            ExchangeImpl<?> exchangeImpl = exchange.exchImpl;
+            if (exchangeImpl != null) {
+                if (exchangeImpl.connection().connected()) {
+                    t = new HttpTimeoutException("request timed out");
+                }
+            }
+        }
+        if (t == null) {
+            t = new HttpConnectTimeoutException("HTTP connect timed out");
+            t.initCause(new ConnectException("HTTP connect timed out"));
+        }
+        multiExchange.cancel(t);
+    }
+
+    @Override
+    public String toString() {
+        return "ResponseTimerEvent[" + super.toString() + "]";
+    }
+}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/TimeoutEvent.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/TimeoutEvent.java	Thu Aug 09 11:23:12 2018 +0100
@@ -43,9 +43,11 @@
     // we use id in compareTo to make compareTo consistent with equals
     // see TimeoutEvent::compareTo below;
     private final long id = COUNTER.incrementAndGet();
+    private final Duration duration;
     private final Instant deadline;
 
     TimeoutEvent(Duration duration) {
+        this.duration = duration;
         deadline = Instant.now().plus(duration);
     }
 
@@ -75,6 +77,7 @@
 
     @Override
     public String toString() {
-        return "TimeoutEvent[id=" + id + ", deadline=" + deadline + "]";
+        return "TimeoutEvent[id=" + id + ", duration=" + duration
+                + ", deadline=" + deadline + "]";
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/AbstractConnectTimeout.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,257 @@
+/*
+ * 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.
+ */
+
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.NoRouteToHostException;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpConnectTimeoutException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletionException;
+import org.testng.annotations.DataProvider;
+import static java.lang.System.out;
+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;
+import static java.time.Duration.*;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.testng.Assert.fail;
+
+public abstract class AbstractConnectTimeout {
+
+    static final Duration NO_DURATION = null;
+
+    static List<List<Duration>> TIMEOUTS = List.of(
+                    // connectTimeout   HttpRequest timeout
+            Arrays.asList( NO_DURATION,   ofSeconds(1)  ),
+            Arrays.asList( NO_DURATION,   ofMillis(100) ),
+            Arrays.asList( NO_DURATION,   ofNanos(99)   ),
+            Arrays.asList( NO_DURATION,   ofNanos(1)    ),
+
+            Arrays.asList( ofSeconds(1),  NO_DURATION   ),
+            Arrays.asList( ofMillis(100), NO_DURATION   ),
+            Arrays.asList( ofNanos(99),   NO_DURATION   ),
+            Arrays.asList( ofNanos(1),    NO_DURATION   ),
+
+            Arrays.asList( ofSeconds(1),  ofMinutes(1)  ),
+            Arrays.asList( ofMillis(100), ofMinutes(1)  ),
+            Arrays.asList( ofNanos(99),   ofMinutes(1)  ),
+            Arrays.asList( ofNanos(1),    ofMinutes(1)  )
+    );
+
+    static final List<String> METHODS = List.of("GET", "POST");
+    static final List<Version> VERSIONS = List.of(HTTP_2, HTTP_1_1);
+    static final List<String> SCHEMES = List.of("https", "http");
+
+    @DataProvider(name = "variants")
+    public Object[][] variants() {
+        List<Object[]> l = new ArrayList<>();
+        for (List<Duration> timeouts : TIMEOUTS) {
+           Duration connectTimeout = timeouts.get(0);
+           Duration requestTimeout = timeouts.get(1);
+           for (String method: METHODS) {
+            for (String scheme : SCHEMES) {
+             for (Version requestVersion : VERSIONS) {
+              l.add(new Object[] {requestVersion, scheme, method, connectTimeout, requestTimeout});
+        }}}}
+        return l.stream().toArray(Object[][]::new);
+    }
+
+    static final ProxySelector EXAMPLE_DOT_COM_PROXY = ProxySelector.of(
+            InetSocketAddress.createUnresolved("example.com", 8080));
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutNoProxySync(Version requestVersion,
+                                      String scheme,
+                                      String method,
+                                      Duration connectTimeout,
+                                      Duration requestTimeout)
+        throws Exception
+    {
+        timeoutSync(requestVersion, scheme, method, connectTimeout, requestTimeout, NO_PROXY);
+    }
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutWithProxySync(Version requestVersion,
+                                        String scheme,
+                                        String method,
+                                        Duration connectTimeout,
+                                        Duration requestTimeout)
+        throws Exception
+    {
+        timeoutSync(requestVersion, scheme, method, connectTimeout, requestTimeout, EXAMPLE_DOT_COM_PROXY);
+    }
+
+    private void timeoutSync(Version requestVersion,
+                             String scheme,
+                             String method,
+                             Duration connectTimeout,
+                             Duration requestTimeout,
+                             ProxySelector proxy)
+        throws Exception
+    {
+        out.printf("%ntimeoutSync(requestVersion=%s, scheme=%s, method=%s,"
+                   + " connectTimeout=%s, requestTimeout=%s, proxy=%s)%n",
+                   requestVersion, scheme, method, connectTimeout, requestTimeout, proxy);
+
+        HttpClient client = newClient(connectTimeout, proxy);
+        HttpRequest request = newRequest(scheme, requestVersion, method, requestTimeout);
+
+        for (int i = 0; i < 2; i++) {
+            out.printf("iteration %d%n", i);
+            long startTime = System.nanoTime();
+            try {
+                HttpResponse<?> resp = client.send(request, BodyHandlers.ofString());
+                printResponse(resp);
+                fail("Unexpected response: " + resp);
+            } catch (HttpConnectTimeoutException expected) { // blocking thread-specific exception
+                long elapsedTime = NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                out.printf("Client: received in %d millis%n", elapsedTime);
+                assertExceptionTypeAndCause(expected.getCause());
+            } catch (ConnectException e) {
+                long elapsedTime = NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                out.printf("Client: received in %d millis%n", elapsedTime);
+                Throwable t = e.getCause().getCause();  // blocking thread-specific exception
+                if (!(t instanceof NoRouteToHostException)) { // tolerate only NRTHE
+                    e.printStackTrace(out);
+                    fail("Unexpected exception:" + e);
+                } else {
+                    out.printf("Caught ConnectException with NoRouteToHostException"
+                            + " cause: %s - skipping%n", t.getCause());
+                }
+            }
+        }
+    }
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutNoProxyAsync(Version requestVersion,
+                                       String scheme,
+                                       String method,
+                                       Duration connectTimeout,
+                                       Duration requestTimeout) {
+        timeoutAsync(requestVersion, scheme, method, connectTimeout, requestTimeout, NO_PROXY);
+    }
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutWithProxyAsync(Version requestVersion,
+                                         String scheme,
+                                         String method,
+                                         Duration connectTimeout,
+                                         Duration requestTimeout) {
+        timeoutAsync(requestVersion, scheme, method, connectTimeout, requestTimeout, EXAMPLE_DOT_COM_PROXY);
+    }
+
+    private void timeoutAsync(Version requestVersion,
+                              String scheme,
+                              String method,
+                              Duration connectTimeout,
+                              Duration requestTimeout,
+                              ProxySelector proxy) {
+        out.printf("%ntimeoutAsync(requestVersion=%s, scheme=%s, method=%s, "
+                   + "connectTimeout=%s, requestTimeout=%s, proxy=%s)%n",
+                   requestVersion, scheme, method, connectTimeout, requestTimeout, proxy);
+
+        HttpClient client = newClient(connectTimeout, proxy);
+        HttpRequest request = newRequest(scheme, requestVersion, method, requestTimeout);
+        for (int i = 0; i < 2; i++) {
+            out.printf("iteration %d%n", i);
+            long startTime = System.nanoTime();
+            try {
+                HttpResponse<?> resp = client.sendAsync(request, BodyHandlers.ofString()).join();
+                printResponse(resp);
+                fail("Unexpected response: " + resp);
+            } catch (CompletionException e) {
+                long elapsedTime = NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                out.printf("Client: received in %d millis%n", elapsedTime);
+                Throwable t = e.getCause();
+                if (t instanceof ConnectException &&
+                        t.getCause() instanceof NoRouteToHostException) { // tolerate only NRTHE
+                    out.printf("Caught ConnectException with NoRouteToHostException"
+                            + " cause: %s - skipping%n", t.getCause());
+                } else {
+                    assertExceptionTypeAndCause(t);
+                }
+            }
+        }
+    }
+
+    static HttpClient newClient(Duration connectTimeout, ProxySelector proxy) {
+        HttpClient.Builder builder = HttpClient.newBuilder().proxy(proxy);
+        if (connectTimeout != NO_DURATION)
+            builder.connectTimeout(connectTimeout);
+        return builder.build();
+    }
+
+    static HttpRequest newRequest(String scheme,
+                                  Version reqVersion,
+                                  String method,
+                                  Duration requestTimeout) {
+        // Resolvable address. Most tested environments just ignore the TCP SYN,
+        // or occasionally return ICMP no route to host
+        URI uri = URI.create(scheme +"://example.com:81/");
+        HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(uri);
+        reqBuilder = reqBuilder.version(reqVersion);
+        switch (method) {
+            case "GET"   : reqBuilder.GET();                         break;
+            case "POST"  : reqBuilder.POST(BodyPublishers.noBody()); break;
+            default: throw new AssertionError("Unknown method:" + method);
+        }
+        if (requestTimeout != NO_DURATION)
+            reqBuilder.timeout(requestTimeout);
+        return reqBuilder.build();
+    }
+
+    static void assertExceptionTypeAndCause(Throwable t) {
+        if (!(t instanceof HttpConnectTimeoutException)) {
+            t.printStackTrace(out);
+            fail("Expected HttpConnectTimeoutException, got:" + t);
+        }
+        Throwable connEx = t.getCause();
+        if (!(connEx instanceof ConnectException)) {
+            t.printStackTrace(out);
+            fail("Expected ConnectException cause in:" + connEx);
+        }
+        out.printf("Caught expected HttpConnectTimeoutException with ConnectException"
+                + " cause: %n%s%n%s%n", t, connEx);
+        final String EXPECTED_MESSAGE = "HTTP connect timed out"; // impl dependent
+        if (!connEx.getMessage().equals(EXPECTED_MESSAGE))
+            fail("Expected: \"" + EXPECTED_MESSAGE + "\", got: \"" + connEx.getMessage() + "\"");
+
+    }
+
+    static void printResponse(HttpResponse<?> response) {
+        out.println("Unexpected response: " + response);
+        out.println("Headers: " + response.headers());
+        out.println("Body: " + response.body());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/AbstractConnectTimeoutHandshake.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpConnectTimeoutException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import static java.lang.String.format;
+import static java.lang.System.out;
+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;
+import static java.time.Duration.*;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.testng.Assert.fail;
+
+public abstract class AbstractConnectTimeoutHandshake {
+
+    // The number of iterations each testXXXClient performs.
+    static final int TIMES = 2;
+
+    Server server;
+    URI httpsURI;
+
+    static final Duration NO_DURATION = null;
+
+    static List<List<Duration>> TIMEOUTS = List.of(
+                    // connectTimeout   HttpRequest timeout
+            Arrays.asList( NO_DURATION,   ofSeconds(1)  ),
+            Arrays.asList( NO_DURATION,   ofSeconds(2)  ),
+            Arrays.asList( NO_DURATION,   ofMillis(500) ),
+
+            Arrays.asList( ofSeconds(1),  NO_DURATION   ),
+            Arrays.asList( ofSeconds(2),  NO_DURATION   ),
+            Arrays.asList( ofMillis(500), NO_DURATION   ),
+
+            Arrays.asList( ofSeconds(1),  ofMinutes(1)  ),
+            Arrays.asList( ofSeconds(2),  ofMinutes(1)  ),
+            Arrays.asList( ofMillis(500), ofMinutes(1)  )
+    );
+
+    static final List<String> METHODS = List.of("GET" , "POST");
+    static final List<Version> VERSIONS = List.of(HTTP_2, HTTP_1_1);
+
+    @DataProvider(name = "variants")
+    public Object[][] variants() {
+        List<Object[]> l = new ArrayList<>();
+        for (List<Duration> timeouts : TIMEOUTS) {
+           Duration connectTimeout = timeouts.get(0);
+           Duration requestTimeout = timeouts.get(1);
+           for (String method: METHODS) {
+            for (Version requestVersion : VERSIONS) {
+             l.add(new Object[] {requestVersion, method, connectTimeout, requestTimeout});
+        }}}
+        return l.stream().toArray(Object[][]::new);
+    }
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutSync(Version requestVersion,
+                              String method,
+                              Duration connectTimeout,
+                              Duration requestTimeout)
+        throws Exception
+    {
+        out.printf("%n--- timeoutSync requestVersion=%s, method=%s, "
+                   + "connectTimeout=%s, requestTimeout=%s ---%n",
+                   requestVersion, method, connectTimeout, requestTimeout);
+        HttpClient client = newClient(connectTimeout);
+        HttpRequest request = newRequest(requestVersion, method, requestTimeout);
+
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            long startTime = System.nanoTime();
+            try {
+                HttpResponse<String> resp = client.send(request, BodyHandlers.ofString());
+                printResponse(resp);
+                fail("Unexpected response: " + resp);
+            } catch (HttpConnectTimeoutException expected) {
+                long elapsedTime = NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                out.printf("Client: received in %d millis%n", elapsedTime);
+                out.printf("Client: caught expected HttpConnectTimeoutException: %s%n", expected);
+                checkExceptionOrCause(ConnectException.class, expected);
+            }
+        }
+    }
+
+    //@Test(dataProvider = "variants")
+    protected void timeoutAsync(Version requestVersion,
+                                String method,
+                                Duration connectTimeout,
+                                Duration requestTimeout) {
+        out.printf("%n--- timeoutAsync requestVersion=%s, method=%s, "
+                        + "connectTimeout=%s, requestTimeout=%s ---%n",
+                   requestVersion, method, connectTimeout, requestTimeout);
+        HttpClient client = newClient(connectTimeout);
+        HttpRequest request = newRequest(requestVersion, method, requestTimeout);
+
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            long startTime = System.nanoTime();
+            CompletableFuture<HttpResponse<String>> cf =
+                    client.sendAsync(request, BodyHandlers.ofString());
+            try {
+                HttpResponse<String> resp = cf.join();
+                printResponse(resp);
+                fail("Unexpected response: " + resp);
+            } catch (CompletionException ce) {
+                long elapsedTime = NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                out.printf("Client: received in %d millis%n", elapsedTime);
+                Throwable expected = ce.getCause();
+                if (expected instanceof HttpConnectTimeoutException) {
+                    out.printf("Client: caught expected HttpConnectTimeoutException: %s%n", expected);
+                    checkExceptionOrCause(ConnectException.class, expected);
+                } else {
+                    out.printf("Client: caught UNEXPECTED exception: %s%n", expected);
+                    throw ce;
+                }
+            }
+        }
+    }
+
+    static HttpClient newClient(Duration connectTimeout) {
+        HttpClient.Builder builder = HttpClient.newBuilder().proxy(NO_PROXY);
+        if (connectTimeout != NO_DURATION)
+            builder.connectTimeout(connectTimeout);
+        return builder.build();
+    }
+
+    HttpRequest newRequest(Version reqVersion,
+                           String method,
+                           Duration requestTimeout) {
+        HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(httpsURI);
+        reqBuilder = reqBuilder.version(reqVersion);
+        switch (method) {
+            case "GET"   : reqBuilder.GET();                         break;
+            case "POST"  : reqBuilder.POST(BodyPublishers.noBody()); break;
+            default: throw new AssertionError("Unknown method:" + method);
+        }
+        if (requestTimeout != NO_DURATION)
+            reqBuilder.timeout(requestTimeout);
+        return reqBuilder.build();
+    }
+
+    static void checkExceptionOrCause(Class<? extends Throwable> clazz, Throwable t) {
+        final Throwable original = t;
+        do {
+            if (clazz.isInstance(t)) {
+                System.out.println("Found expected exception/cause: " + t);
+                return; // found
+            }
+        } while ((t = t.getCause()) != null);
+        original.printStackTrace(System.out);
+        throw new RuntimeException("Expected " + clazz + "in " + original);
+    }
+
+    static void printResponse(HttpResponse<?> response) {
+        out.println("Unexpected response: " + response);
+        out.println("Headers: " + response.headers());
+        out.println("Body: " + response.body());
+    }
+
+    // -- Infrastructure
+
+    static String serverAuthority(Server server) {
+        return InetAddress.getLoopbackAddress().getHostName() + ":"
+                + server.getPort();
+    }
+
+    @BeforeTest
+    public void setup() throws Exception {
+        server = new Server();
+        httpsURI = URI.create("https://" + serverAuthority(server) + "/foo");
+        out.println("HTTPS URI: " + httpsURI);
+    }
+
+    @AfterTest
+    public void teardown() throws Exception {
+        server.close();
+        out.printf("%n--- teardown ---%n");
+
+        int numClientConnections = variants().length * TIMES;
+        int serverCount = server.count;
+        out.printf("Client made %d connections.%n", numClientConnections);
+        out.printf("Server received %d connections.%n", serverCount);
+
+        // This is usually the case, but not always, do not assert. Remains
+        // as an informative message only.
+        //if (numClientConnections != serverCount)
+        //    fail(format("[numTests: %d] != [serverCount: %d]",
+        //                numClientConnections, serverCount));
+    }
+
+    /**
+     * Emulates a server-side, using plain cleartext Sockets, that just reads
+     * initial client hello and does nothing more.
+     */
+    static class Server extends Thread implements AutoCloseable {
+        private final ServerSocket ss;
+        private volatile boolean closed;
+        volatile int count;
+
+        Server() throws IOException {
+            super("Server");
+            ss = new ServerSocket();
+            ss.setReuseAddress(false);
+            ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+            this.start();
+        }
+
+        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);
+            }
+        }
+
+        @Override
+        public void run() {
+            while (!closed) {
+                try (Socket s = ss.accept()) {
+                    count++;
+                    out.println("Server: accepted new connection");
+                    InputStream is = new BufferedInputStream(s.getInputStream());
+
+                    out.println("Server: starting to read");
+                    while (is.read() != -1) ;
+
+                    out.println("Server: closing connection");
+                    s.close(); // close without giving any reply
+                } catch (IOException e) {
+                    if (!closed)
+                        out.println("UNEXPECTED " + e);
+                }
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutHandshakeAsync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests connection timeouts during SSL handshake
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutHandshakeAsync
+ */
+
+public class ConnectTimeoutHandshakeAsync
+    extends AbstractConnectTimeoutHandshake
+{
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutAsync(Version requestVersion,
+                             String method,
+                             Duration connectTimeout,
+                             Duration requestTimeout) {
+        super.timeoutAsync(requestVersion, method, connectTimeout, requestTimeout);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutHandshakeSync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests connection timeouts during SSL handshake
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutHandshakeSync
+ */
+
+public class ConnectTimeoutHandshakeSync
+    extends AbstractConnectTimeoutHandshake
+{
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutSync(Version requestVersion,
+                            String method,
+                            Duration connectTimeout,
+                            Duration requestTimeout)
+        throws Exception
+    {
+        super.timeoutSync(requestVersion, method, connectTimeout, requestTimeout);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutNoProxyAsync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests for connection related timeouts
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutNoProxyAsync
+ */
+
+public class ConnectTimeoutNoProxyAsync extends AbstractConnectTimeout {
+
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutNoProxyAsync(Version requestVersion,
+                                    String scheme,
+                                    String method,
+                                    Duration connectTimeout,
+                                    Duration requestduration)
+    {
+        super.timeoutNoProxyAsync(requestVersion, scheme, method, connectTimeout, requestduration);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutNoProxySync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests for connection related timeouts
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutNoProxySync
+ */
+
+public class ConnectTimeoutNoProxySync extends AbstractConnectTimeout {
+
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutNoProxySync(Version requestVersion,
+                                   String scheme,
+                                   String method,
+                                   Duration connectTimeout,
+                                   Duration requestTimeout)
+        throws Exception
+    {
+        super.timeoutNoProxySync(requestVersion, scheme, method, connectTimeout, requestTimeout);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutWithProxyAsync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests for connection related timeouts
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutWithProxyAsync
+ */
+
+public class ConnectTimeoutWithProxyAsync extends AbstractConnectTimeout {
+
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutWithProxyAsync(Version requestVersion,
+                                      String scheme,
+                                      String method,
+                                      Duration connectTimeout,
+                                      Duration requestTimeout)
+    {
+        super.timeoutWithProxyAsync(requestVersion, scheme, method, connectTimeout, requestTimeout);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectTimeoutWithProxySync.java	Thu Aug 09 11:23:12 2018 +0100
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+import java.net.http.HttpClient.Version;
+import java.time.Duration;
+import org.testng.annotations.Test;
+
+/*
+ * @test
+ * @summary Tests for connection related timeouts
+ * @bug 8208391
+ * @run testng/othervm ConnectTimeoutWithProxySync
+ */
+
+public class ConnectTimeoutWithProxySync extends AbstractConnectTimeout {
+
+    @Test(dataProvider = "variants")
+    @Override
+    public void timeoutWithProxySync(Version requestVersion,
+                                     String scheme,
+                                     String method,
+                                     Duration connectTimeout,
+                                     Duration requestTimeout)
+        throws Exception
+    {
+        super.timeoutWithProxySync(requestVersion, scheme, method, connectTimeout, requestTimeout);
+    }
+}
--- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java	Thu Aug 09 11:23:12 2018 +0100
@@ -50,6 +50,7 @@
 import java.net.http.HttpClient.Version;
 import jdk.testlibrary.SimpleSSLContext;
 import org.testng.annotations.Test;
+import static java.time.Duration.*;
 import static org.testng.Assert.*;
 
 /*
@@ -74,6 +75,7 @@
             // Empty optionals and defaults
             assertFalse(client.authenticator().isPresent());
             assertFalse(client.cookieHandler().isPresent());
+            assertFalse(client.connectTimeout().isPresent());
             assertFalse(client.executor().isPresent());
             assertFalse(client.proxy().isPresent());
             assertTrue(client.sslParameters() != null);
@@ -88,6 +90,7 @@
         HttpClient.Builder builder = HttpClient.newBuilder();
         assertThrows(NPE, () -> builder.authenticator(null));
         assertThrows(NPE, () -> builder.cookieHandler(null));
+        assertThrows(NPE, () -> builder.connectTimeout(null));
         assertThrows(NPE, () -> builder.executor(null));
         assertThrows(NPE, () -> builder.proxy(null));
         assertThrows(NPE, () -> builder.sslParameters(null));
@@ -128,6 +131,26 @@
         assertTrue(builder.build().cookieHandler().get() == c);
     }
 
+    @Test
+    public void testConnectTimeout() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        Duration a = Duration.ofSeconds(5);
+        builder.connectTimeout(a);
+        assertTrue(builder.build().connectTimeout().get() == a);
+        Duration b = Duration.ofMinutes(1);
+        builder.connectTimeout(b);
+        assertTrue(builder.build().connectTimeout().get() == b);
+        assertThrows(NPE, () -> builder.cookieHandler(null));
+        Duration c = Duration.ofHours(100);
+        builder.connectTimeout(c);
+        assertTrue(builder.build().connectTimeout().get() == c);
+
+        assertThrows(IAE, () -> builder.connectTimeout(ZERO));
+        assertThrows(IAE, () -> builder.connectTimeout(ofSeconds(0)));
+        assertThrows(IAE, () -> builder.connectTimeout(ofSeconds(-1)));
+        assertThrows(IAE, () -> builder.connectTimeout(ofNanos(-100)));
+    }
+
     static class TestExecutor implements Executor {
         public void execute(Runnable r) { }
     }
@@ -292,6 +315,7 @@
 
     static class MockHttpClient extends HttpClient {
         @Override public Optional<CookieHandler> cookieHandler() { return null; }
+        @Override public Optional<Duration> connectTimeout() { return null; }
         @Override public Redirect followRedirects() { return null; }
         @Override public Optional<ProxySelector> proxy() { return null; }
         @Override public SSLContext sslContext() { return null; }
--- a/test/jdk/java/net/httpclient/TimeoutBasic.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/test/jdk/java/net/httpclient/TimeoutBasic.java	Thu Aug 09 11:23:12 2018 +0100
@@ -164,6 +164,7 @@
                     throw new RuntimeException("Unexpected response: " + resp.statusCode());
                 } catch (CompletionException e) {
                     if (!(e.getCause() instanceof HttpTimeoutException)) {
+                        e.printStackTrace(out);
                         throw new RuntimeException("Unexpected exception: " + e.getCause());
                     } else {
                         out.println("Caught expected timeout: " + e.getCause());
--- a/test/jdk/java/net/httpclient/examples/JavadocExamples.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/test/jdk/java/net/httpclient/examples/JavadocExamples.java	Thu Aug 09 11:23:12 2018 +0100
@@ -64,6 +64,7 @@
         HttpClient client = HttpClient.newBuilder()
                 .version(Version.HTTP_1_1)
                 .followRedirects(Redirect.NORMAL)
+                .connectTimeout(Duration.ofSeconds(20))
                 .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
                 .authenticator(Authenticator.getDefault())
                 .build();
@@ -74,7 +75,7 @@
         //Asynchronous Example
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create("https://foo.com/"))
-                .timeout(Duration.ofMinutes(1))
+                .timeout(Duration.ofMinutes(2))
                 .header("Content-Type", "application/json")
                 .POST(BodyPublishers.ofFile(Paths.get("file.json")))
                 .build();
--- a/test/jdk/java/net/httpclient/offline/DelegatingHttpClient.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/test/jdk/java/net/httpclient/offline/DelegatingHttpClient.java	Thu Aug 09 11:23:12 2018 +0100
@@ -27,6 +27,7 @@
 import java.net.Authenticator;
 import java.net.CookieHandler;
 import java.net.ProxySelector;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
@@ -51,6 +52,11 @@
     }
 
     @Override
+    public Optional<Duration> connectTimeout() {
+        return client.connectTimeout();
+    }
+
+    @Override
     public Redirect followRedirects() {
         return client.followRedirects();
     }
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java	Wed Aug 08 15:51:08 2018 -0700
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java	Thu Aug 09 11:23:12 2018 +0100
@@ -31,6 +31,7 @@
 import java.net.ProxySelector;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
+import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
@@ -284,7 +285,8 @@
 
         // All these throw errors
         @Override public HttpPublisher publisher() {return error();}
-        @Override public CompletableFuture<Void> connectAsync() {return error();}
+        @Override public CompletableFuture<Void> connectAsync(Exchange<?> e) {return error();}
+        @Override public CompletableFuture<Void> finishConnect() {return error();}
         @Override SocketChannel channel() {return error();}
         @Override
         FlowTube getConnectionFlow() {return flow;}
@@ -296,6 +298,7 @@
         }
         final ConnectionPool pool;
         @Override public Optional<CookieHandler> cookieHandler() {return error();}
+        @Override public Optional<Duration> connectTimeout() {return error();}
         @Override public HttpClient.Redirect followRedirects() {return error();}
         @Override public Optional<ProxySelector> proxy() {return error();}
         @Override public SSLContext sslContext() {return error();}