http-client-branch: intial load from jdk10/sandbox http-client-branch
authorchegar
Sun, 05 Nov 2017 17:32:13 +0000
branchhttp-client-branch
changeset 55763 634d8e14c172
parent 55762 e947a3a50a95
child 55764 34d7cc00f87a
http-client-branch: intial load from jdk10/sandbox
src/java.base/share/classes/jdk/internal/misc/InnocuousThread.java
src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedInputStream.java
src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthInputStream.java
src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractSubscription.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncTriggerEvent.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/BufferingSubscriber.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/DefaultPublisher.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExecutorWrapper.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1AsyncReceiver.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1HeaderParser.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientFacade.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiMapResult.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PrivilegedExecutor.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestProcessors.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestPublishers.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseProcessors.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseSubscribers.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SocketTube.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncDataReadQueue.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncWriteQueue.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/ConnectionExpiredException.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Demand.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/FlowTube.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/HttpHeadersImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Log.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/MinimalFuture.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLFlowDelegate.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLTube.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SequentialScheduler.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriberWrapper.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriptionBase.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SynchronousPublisher.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/TemporarySubscription.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/DataFrame.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/FramesDecoder.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/HeaderFrame.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Decoder.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/DecodingCallback.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HPACK.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Huffman.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/ISO_8859_1.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IndexNameValueWriter.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IntegerReader.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringReader.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringWriter.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/CooperativeHandler.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/Receiver.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java
src/jdk.incubator.httpclient/share/classes/module-info.java
test/jdk/java/net/httpclient/APIErrors.java
test/jdk/java/net/httpclient/BasicAuthTest.java
test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java
test/jdk/java/net/httpclient/BufferingSubscriberTest.java
test/jdk/java/net/httpclient/CustomRequestPublisher.java
test/jdk/java/net/httpclient/HandshakeFailureTest.java
test/jdk/java/net/httpclient/HeadersTest2.java
test/jdk/java/net/httpclient/HttpClientBuilderTest.java
test/jdk/java/net/httpclient/HttpInputStreamTest.java
test/jdk/java/net/httpclient/HttpRequestBuilderTest.java
test/jdk/java/net/httpclient/LightWeightHttpServer.java
test/jdk/java/net/httpclient/ManyRequests.java
test/jdk/java/net/httpclient/ManyRequests2.java
test/jdk/java/net/httpclient/ManyRequestsLegacy.java
test/jdk/java/net/httpclient/MockServer.java
test/jdk/java/net/httpclient/MultiAuthTest.java
test/jdk/java/net/httpclient/RequestBodyTest.java
test/jdk/java/net/httpclient/RequestBuilderTest.java
test/jdk/java/net/httpclient/RequestProcessorExceptions.java
test/jdk/java/net/httpclient/Server.java
test/jdk/java/net/httpclient/ShortRequestBody.java
test/jdk/java/net/httpclient/SmallTimeout.java
test/jdk/java/net/httpclient/SmokeTest.java
test/jdk/java/net/httpclient/SplitResponse.java
test/jdk/java/net/httpclient/SplitResponseSSL.java
test/jdk/java/net/httpclient/TimeoutOrdering.java
test/jdk/java/net/httpclient/VersionTest.java
test/jdk/java/net/httpclient/http2/BasicTest.java
test/jdk/java/net/httpclient/http2/ErrorTest.java
test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java
test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java
test/jdk/java/net/httpclient/http2/HpackDriver.java
test/jdk/java/net/httpclient/http2/NoBody.java
test/jdk/java/net/httpclient/http2/ProxyTest2.java
test/jdk/java/net/httpclient/http2/RedirectTest.java
test/jdk/java/net/httpclient/http2/ServerPush.java
test/jdk/java/net/httpclient/http2/TLSConnection.java
test/jdk/java/net/httpclient/http2/Timeout.java
test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/BinaryPrimitivesTest.java
test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/DecoderTest.java
test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/EncoderTest.java
test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HeaderTableTest.java
test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HuffmanTest.java
test/jdk/java/net/httpclient/http2/server/BodyInputStream.java
test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java
test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java
test/jdk/java/net/httpclient/http2/server/RedirectHandler.java
test/jdk/java/net/httpclient/security/Driver.java
test/jdk/java/net/httpclient/security/Security.java
test/jdk/java/net/httpclient/security/filePerms/FileProcessorPermissionTest.java
test/jdk/java/net/httpclient/security/filePerms/httpclient.policy
test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java
test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java
test/jdk/java/net/httpclient/whitebox/Driver.java
test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java
test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java
test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java
test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/FlowTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/Http1HeaderParserTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ResponseHeadersTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SSLTubeTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/WrapperTest.java
test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/common/DemandTest.java
--- a/src/java.base/share/classes/jdk/internal/misc/InnocuousThread.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/java.base/share/classes/jdk/internal/misc/InnocuousThread.java	Sun Nov 05 17:32:13 2017 +0000
@@ -62,10 +62,16 @@
      * set to the system class loader.
      */
     public static Thread newThread(String name, Runnable target) {
-        return new InnocuousThread(INNOCUOUSTHREADGROUP,
-                                   target,
-                                   name,
-                                   ClassLoader.getSystemClassLoader());
+        return AccessController.doPrivileged(
+                new PrivilegedAction<Thread>() {
+                    @Override
+                    public Thread run() {
+                        return new InnocuousThread(INNOCUOUSTHREADGROUP,
+                                                   target,
+                                                   name,
+                                                   ClassLoader.getSystemClassLoader());
+                    }
+                });
     }
 
     /**
@@ -80,8 +86,14 @@
      * Returns a new InnocuousThread with null context class loader.
      */
     public static Thread newSystemThread(String name, Runnable target) {
-        return new InnocuousThread(INNOCUOUSTHREADGROUP,
-                                   target, name, null);
+        return AccessController.doPrivileged(
+                new PrivilegedAction<Thread>() {
+                    @Override
+                    public Thread run() {
+                        return new InnocuousThread(INNOCUOUSTHREADGROUP,
+                                                   target, name, null);
+                    }
+                });
     }
 
     private InnocuousThread(ThreadGroup group, Runnable target, String name, ClassLoader tccl) {
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedInputStream.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedInputStream.java	Sun Nov 05 17:32:13 2017 +0000
@@ -135,6 +135,8 @@
             needToReadHeader = true;
             consumeCRLF();
         }
+        if (n < 0 && !eof)
+            throw new IOException("connection closed before all data received");
         return n;
     }
 
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthInputStream.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthInputStream.java	Sun Nov 05 17:32:13 2017 +0000
@@ -60,6 +60,8 @@
                 t.getServerImpl().requestCompleted (t.getConnection());
             }
         }
+        if (n < 0 && !eof)
+            throw new IOException("Connection closed before all bytes received");
         return n;
     }
 
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -797,7 +797,8 @@
     // fashion.
 
     void requestCompleted (HttpConnection c) {
-        assert c.getState() == State.REQUEST;
+        State s = c.getState();
+        assert s == State.REQUEST : "State is not REQUEST ("+s+")";
         reqConnections.remove (c);
         c.rspStartedTime = getTime();
         rspConnections.add (c);
@@ -806,7 +807,8 @@
 
     // called after response has been sent
     void responseCompleted (HttpConnection c) {
-        assert c.getState() == State.RESPONSE;
+        State s = c.getState();
+        assert s == State.RESPONSE : "State is not RESPONSE ("+s+")";
         rspConnections.remove (c);
         c.setState (State.IDLE);
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -28,9 +28,20 @@
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLParameters;
+
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.Log;
 import jdk.incubator.http.internal.common.ExceptionallyCloseable;
+import jdk.incubator.http.internal.common.Utils;
 
 
 /**
@@ -52,34 +63,135 @@
  *
  */
 abstract class AbstractAsyncSSLConnection extends HttpConnection
-               implements AsyncConnection, ExceptionallyCloseable {
-
+    implements ExceptionallyCloseable
+{
+    protected final SSLEngine engine;
+    protected final String serverName;
+    protected final SSLParameters sslParameters;
 
-    AbstractAsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client) {
+    AbstractAsyncSSLConnection(InetSocketAddress addr,
+                               HttpClientImpl client,
+                               String serverName,
+                               String[] alpn) {
         super(addr, client);
+        this.serverName = serverName;
+        SSLContext context = client.theSSLContext();
+        sslParameters = createSSLParameters(client, context, serverName, alpn);
+        Log.logParams(sslParameters);
+        engine = createEngine(context, sslParameters);
+    }
+
+    abstract HttpConnection plainConnection();
+    abstract SSLTube getConnectionFlow();
+
+    final CompletableFuture<String> getALPN() {
+        assert connected();
+        return getConnectionFlow().getALPN();
     }
 
-    abstract SSLEngine getEngine();
-    abstract AsyncSSLDelegate sslDelegate();
-    abstract HttpConnection plainConnection();
-    abstract HttpConnection downgrade();
+    final SSLEngine getEngine() { return engine; }
+
+    @Override
+    SSLParameters sslParameters() { return sslParameters; }
+
+    private static SSLParameters createSSLParameters(HttpClientImpl client,
+                                                     SSLContext context,
+                                                     String serverName,
+                                                     String[] alpn) {
+        SSLParameters sslp = client.sslParameters();
+        SSLParameters sslParameters = Utils.copySSLParameters(sslp);
+        if (alpn != null) {
+            Log.logSSL("AbstractAsyncSSLConnection: Setting application protocols: {0}",
+                       Arrays.toString(alpn));
+            sslParameters.setApplicationProtocols(alpn);
+        } else {
+            Log.logSSL("AbstractAsyncSSLConnection: no applications set!");
+        }
+        if (serverName != null) {
+            sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
+        }
+        return sslParameters;
+    }
+
+    private static SSLEngine createEngine(SSLContext context,
+                                          SSLParameters sslParameters) {
+        SSLEngine engine = context.createSSLEngine();
+        engine.setUseClientMode(true);
+        engine.setSSLParameters(sslParameters);
+        return engine;
+    }
 
     @Override
     final boolean isSecure() {
         return true;
     }
 
-    // Blocking read functions not used here
-    @Override
-    protected final ByteBuffer readImpl() throws IOException {
-        throw new UnsupportedOperationException("Not supported.");
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+    static final class SSLConnectionChannel extends DetachedConnectionChannel {
+        final DetachedConnectionChannel delegate;
+        final SSLDelegate sslDelegate;
+        SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
+            this.delegate = delegate;
+            this.sslDelegate = sslDelegate;
+        }
+
+        SocketChannel channel() {
+            return delegate.channel();
+        }
+
+        @Override
+        ByteBuffer read() throws IOException {
+            SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
+            // TODO: check for closure
+            int n = r.result.bytesProduced();
+            if (n > 0) {
+                return r.buf;
+            } else if (n == 0) {
+                return Utils.EMPTY_BYTEBUFFER;
+            } else {
+                return null;
+            }
+        }
+        @Override
+        long write(ByteBuffer[] buffers, int start, int number) throws IOException {
+            long l = SSLDelegate.countBytes(buffers, start, number);
+            SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
+            if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
+                if (l > 0) {
+                    throw new IOException("SSLHttpConnection closed");
+                }
+            }
+            return l;
+        }
+        @Override
+        public void shutdownInput() throws IOException {
+            delegate.shutdownInput();
+        }
+        @Override
+        public void shutdownOutput() throws IOException {
+            delegate.shutdownOutput();
+        }
+        @Override
+        public void close() {
+            delegate.close();
+        }
     }
 
-    // whenReceivedResponse only used in HTTP/1.1 (Http1Exchange)
-    // AbstractAsyncSSLConnection is only used with HTTP/2
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
     @Override
-    final CompletableFuture<Void> whenReceivingResponse() {
-        throw new UnsupportedOperationException("Not supported.");
+    DetachedConnectionChannel detachChannel() {
+        HttpClientImpl client = client();
+        assert client != null;
+        DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
+        SSLDelegate sslDelegate = new SSLDelegate(engine,
+                                                  detachedChannel.channel(),
+                                                  client,
+                                                  serverName);
+        return new SSLConnectionChannel(detachedChannel, sslDelegate);
     }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractSubscription.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
+
+/**
+ * A {@link Flow.Subscription} wrapping a {@link Demand} instance.
+ *
+ */
+abstract class AbstractSubscription implements Flow.Subscription {
+
+    private final Demand demand = new Demand();
+
+    /**
+     * Returns the subscription's demand.
+     * @return the subscription's demand.
+     */
+    protected Demand demand() { return demand; }
+
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -28,14 +28,11 @@
 import jdk.incubator.http.internal.common.ByteBufferReference;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
 
 /**
  * Implemented by classes that offer an asynchronous interface.
  *
- * PlainHttpConnection, AsyncSSLConnection AsyncSSLDelegate.
+ * PlainHttpConnection, AsyncSSLConnection.
  *
  * setAsyncCallbacks() is called to set the callback for reading
  * and error notification. Reads all happen on the selector thread, which
@@ -51,31 +48,6 @@
 interface AsyncConnection {
 
     /**
-     * Enables asynchronous sending and receiving mode. The given async
-     * receiver will receive all incoming data. asyncInput() will be called
-     * to trigger reads. asyncOutput() will be called to drive writes.
-     *
-     * The errorReceiver callback must be called when any fatal exception
-     * occurs. Connection is assumed to be closed afterwards.
-     */
-    void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-                           Consumer<Throwable> errorReceiver,
-                           Supplier<ByteBufferReference> readBufferSupplier);
-
-
-
-    /**
-     * Does whatever is required to start reading. Usually registers
-     * an event with the selector thread.
-     */
-    void startReading();
-
-    /**
-     * Cancel asynchronous reading. Used to downgrade a HTTP/2 connection to HTTP/1
-     */
-    void stopAsyncReading();
-
-    /**
      * In async mode, this method puts buffers at the end of the send queue.
      * When in async mode, calling this method should later be followed by
      * subsequent flushAsync invocation.
@@ -85,11 +57,6 @@
     void writeAsync(ByteBufferReference[] buffers) throws IOException;
 
     /**
-     * Re-enable asynchronous reads through the callback
-     */
-    void enableCallback();
-
-    /**
      * In async mode, this method may put buffers at the beginning of send queue,
      * breaking frames sequence and allowing to write these buffers before other
      * buffers in the queue.
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,23 +25,24 @@
 
 package jdk.incubator.http;
 
+import java.io.IOException;
 import java.nio.channels.SelectableChannel;
 
 /**
  * Event handling interface from HttpClientImpl's selector.
  *
- * If BLOCKING is set, then the channel will be put in blocking
- * mode prior to handle() being called. If false, then it remains non-blocking.
- *
  * If REPEATING is set then the event is not cancelled after being posted.
  */
 abstract class AsyncEvent {
 
-    public static final int BLOCKING = 0x1; // non blocking if not set
     public static final int REPEATING = 0x2; // one off event if not set
 
     protected final int flags;
 
+    AsyncEvent() {
+        this(0);
+    }
+
     AsyncEvent(int flags) {
         this.flags = flags;
     }
@@ -55,12 +56,13 @@
     /** Called when event occurs */
     public abstract void handle();
 
-    /** Called when selector is shutting down. Abort all exchanges. */
-    public abstract void abort();
-
-    public boolean blocking() {
-        return (flags & BLOCKING) != 0;
-    }
+    /**
+     * Called when an error occurs during registration, or when the selector has
+     * been shut down. Aborts all exchanges.
+     *
+     * @param ioe  the IOException, or null
+     */
+    public abstract void abort(IOException ioe);
 
     public boolean repeating() {
         return (flags & REPEATING) != 0;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,37 +26,30 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
+import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.Utils;
 
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Utils;
 
 /**
  * Asynchronous version of SSLConnection.
  */
 class AsyncSSLConnection extends AbstractAsyncSSLConnection {
 
-    final AsyncSSLDelegate sslDelegate;
     final PlainHttpConnection plainConnection;
-    final String serverName;
+    final PlainHttpPublisher writePublisher;
+    private volatile SSLTube flow;
 
-    AsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
-        super(addr, client);
+    AsyncSSLConnection(InetSocketAddress addr,
+                       HttpClientImpl client,
+                       String[] alpn) {
+        super(addr, client, Utils.getServerName(addr), alpn);
         plainConnection = new PlainHttpConnection(addr, client);
-        serverName = Utils.getServerName(addr);
-        sslDelegate = new AsyncSSLDelegate(plainConnection, client, ap, serverName);
-    }
-
-    @Override
-    synchronized void configureMode(Mode mode) throws IOException {
-        super.configureMode(mode);
-        plainConnection.configureMode(mode);
+        writePublisher = new PlainHttpPublisher();
     }
 
     @Override
@@ -65,30 +58,26 @@
     }
 
     @Override
-    AsyncSSLDelegate sslDelegate() {
-        return sslDelegate;
-    }
-
-    @Override
-    public void connect() throws IOException, InterruptedException {
-        plainConnection.connect();
-        configureMode(Mode.ASYNC);
-        startReading();
-        sslDelegate.connect();
-    }
-
-    @Override
     public CompletableFuture<Void> connectAsync() {
-        // not used currently
-        throw new InternalError();
+        return plainConnection
+                .connectAsync()
+                .thenApply( unused -> {
+                    // create the SSLTube wrapping the SocketTube, with the given engine
+                    flow = new SSLTube(engine,
+                                       client().theExecutor(),
+                                       plainConnection.getConnectionFlow());
+                    return null; } );
     }
 
     @Override
     boolean connected() {
-        return plainConnection.connected() && sslDelegate.connected();
+        return plainConnection.connected();
     }
 
     @Override
+    HttpPublisher publisher() { return writePublisher; }
+
+    @Override
     boolean isProxied() {
         return false;
     }
@@ -99,97 +88,50 @@
     }
 
     @Override
-    public void enableCallback() {
-        sslDelegate.enableCallback();
-    }
-
-    @Override
     ConnectionPool.CacheKey cacheKey() {
         return ConnectionPool.cacheKey(address, null);
     }
 
     @Override
-    long write(ByteBuffer[] buffers, int start, int number)
-        throws IOException
-    {
-        ByteBuffer[] bufs = Utils.reduce(buffers, start, number);
-        long n = Utils.remaining(bufs);
-        sslDelegate.writeAsync(ByteBufferReference.toReferences(bufs));
-        sslDelegate.flushAsync();
-        return n;
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        long n = buffer.remaining();
-        sslDelegate.writeAsync(ByteBufferReference.toReferences(buffer));
-        sslDelegate.flushAsync();
-        return n;
+    public void writeAsync(ByteBufferReference[] buffers) throws IOException {
+        writePublisher.writeAsync(buffers);
     }
 
     @Override
     public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        assert getMode() == Mode.ASYNC;
-        sslDelegate.writeAsyncUnordered(buffers);
-    }
-
-    @Override
-    public void writeAsync(ByteBufferReference[] buffers) throws IOException {
-        assert getMode() == Mode.ASYNC;
-        sslDelegate.writeAsync(buffers);
+        writePublisher.writeAsyncUnordered(buffers);
     }
 
     @Override
     public void flushAsync() throws IOException {
-        sslDelegate.flushAsync();
+        writePublisher.flushAsync();
     }
 
     @Override
     public void closeExceptionally(Throwable cause) {
-        Utils.close(cause, sslDelegate, plainConnection.channel());
+        debug.log(Level.DEBUG, () -> "closing: " + cause);
+        plainConnection.close();
     }
 
     @Override
     public void close() {
-        Utils.close(sslDelegate, plainConnection.channel());
+        plainConnection.close();
     }
 
     @Override
     void shutdownInput() throws IOException {
+        debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
         plainConnection.channel().shutdownInput();
     }
 
     @Override
     void shutdownOutput() throws IOException {
+        debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
         plainConnection.channel().shutdownOutput();
     }
 
-    @Override
-    SSLEngine getEngine() {
-        return sslDelegate.getEngine();
-    }
-
-    @Override
-    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-                                  Consumer<Throwable> errorReceiver,
-                                  Supplier<ByteBufferReference> readBufferSupplier) {
-        sslDelegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
-        plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
-    }
-
-    @Override
-    public void startReading() {
-        plainConnection.startReading();
-        sslDelegate.startReading();
-    }
-
-    @Override
-    public void stopAsyncReading() {
-        plainConnection.stopAsyncReading();
-    }
-
-    @Override
-    SSLConnection downgrade() {
-        return new SSLConnection(this);
-    }
+   @Override
+   SSLTube getConnectionFlow() {
+       return flow;
+   }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLDelegate.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,692 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, 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.incubator.http;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import static javax.net.ssl.SSLEngineResult.Status.*;
-import javax.net.ssl.*;
-
-import jdk.incubator.http.internal.common.AsyncWriteQueue;
-import jdk.incubator.http.internal.common.ByteBufferPool;
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Queue;
-import jdk.incubator.http.internal.common.Utils;
-import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
-import jdk.incubator.http.internal.common.ExceptionallyCloseable;
-
-/**
- * Asynchronous wrapper around SSLEngine. send and receive is fully non
- * blocking. When handshaking is required, a thread is created to perform
- * the handshake and application level sends do not take place during this time.
- *
- * Is implemented using queues and functions operating on the receiving end
- * of each queue.
- *
- * Application writes to:
- *        ||
- *        \/
- *     appOutputQ
- *        ||
- *        \/
- * appOutputQ read by "upperWrite" method which does SSLEngine.wrap
- * and does async write to PlainHttpConnection
- *
- * Reading side is as follows
- * --------------------------
- *
- * "upperRead" method reads off channelInputQ and calls SSLEngine.unwrap and
- * when decrypted data is returned, it is passed to the user's Consumer<ByteBuffer>
- *        /\
- *        ||
- *     channelInputQ
- *        /\
- *        ||
- * "asyncReceive" method puts buffers into channelInputQ. It is invoked from
- * OP_READ events from the selector.
- *
- * Whenever handshaking is required, the doHandshaking() method is called
- * which creates a thread to complete the handshake. It takes over the
- * channelInputQ from upperRead, and puts outgoing packets on channelOutputQ.
- * Selector events are delivered to asyncReceive and lowerWrite as normal.
- *
- * Errors
- *
- * Any exception thrown by the engine or channel, causes all Queues to be closed
- * the channel to be closed, and the error is reported to the user's
- * Consumer<Throwable>
- */
-class AsyncSSLDelegate implements ExceptionallyCloseable, AsyncConnection {
-
-    // outgoing buffers put in this queue first and may remain here
-    // while SSL handshaking happening.
-    final AsyncWriteQueue appOutputQ = new AsyncWriteQueue(this::upperWrite);
-
-    // Bytes read into this queue before being unwrapped. Backup on this
-    // Q should only happen when the engine is stalled due to delegated tasks
-    final Queue<ByteBufferReference> channelInputQ;
-
-    // input occurs through the read() method which is expected to be called
-    // when the selector signals some data is waiting to be read. All incoming
-    // handshake data is handled in this method, which means some calls to
-    // read() may return zero bytes of user data. This is not a sign of spinning,
-    // just that the handshake mechanics are being executed.
-
-    final SSLEngine engine;
-    final SSLParameters sslParameters;
-    final HttpConnection lowerOutput;
-    final HttpClientImpl client;
-    final String serverName;
-    // should be volatile to provide proper synchronization(visibility) action
-    volatile Consumer<ByteBufferReference> asyncReceiver;
-    volatile Consumer<Throwable> errorHandler;
-    volatile boolean connected = false;
-
-    // Locks.
-    final Object reader = new Object();
-    // synchronizing handshake state
-    final Semaphore handshaker = new Semaphore(1);
-    final String[] alpn;
-
-    // alpn[] may be null. upcall is callback which receives incoming decoded bytes off socket
-
-    AsyncSSLDelegate(HttpConnection lowerOutput, HttpClientImpl client, String[] alpn, String sname)
-    {
-        SSLContext context = client.sslContext();
-        this.serverName = sname;
-        engine = context.createSSLEngine();
-        engine.setUseClientMode(true);
-        SSLParameters sslp = client.sslParameters()
-                                   .orElseGet(context::getSupportedSSLParameters);
-        sslParameters = Utils.copySSLParameters(sslp);
-        if (alpn != null) {
-            Log.logSSL("AsyncSSLDelegate: Setting application protocols: " + Arrays.toString(alpn));
-            sslParameters.setApplicationProtocols(alpn);
-        } else {
-            Log.logSSL("AsyncSSLDelegate: no applications set!");
-        }
-        if (serverName != null) {
-            SNIHostName sn = new SNIHostName(serverName);
-            sslParameters.setServerNames(List.of(sn));
-        }
-        logParams(sslParameters);
-        engine.setSSLParameters(sslParameters);
-        this.lowerOutput = lowerOutput;
-        this.client = client;
-        this.channelInputQ = new Queue<>();
-        this.channelInputQ.registerPutCallback(this::upperRead);
-        this.alpn = alpn;
-    }
-
-    @Override
-    public void writeAsync(ByteBufferReference[] src) throws IOException {
-        appOutputQ.put(src);
-    }
-
-    @Override
-    public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        appOutputQ.putFirst(buffers);
-    }
-
-    @Override
-    public void flushAsync() throws IOException {
-        if (appOutputQ.flush()) {
-            lowerOutput.flushAsync();
-        }
-    }
-
-    SSLEngine getEngine() {
-        return engine;
-    }
-
-    @Override
-    public void closeExceptionally(Throwable t) {
-        Utils.close(t, appOutputQ, channelInputQ, lowerOutput);
-    }
-
-    @Override
-    public void close() {
-        Utils.close(appOutputQ, channelInputQ, lowerOutput);
-    }
-
-    // The code below can be uncommented to shake out
-    // the implementation by inserting random delays and trigger
-    // handshake in the SelectorManager thread (upperRead)
-    // static final java.util.Random random =
-    //    new java.util.Random(System.currentTimeMillis());
-
-    /**
-     * Attempts to wrap buffers from appOutputQ and place them on the
-     * channelOutputQ for writing. If handshaking is happening, then the
-     * process stalls and last buffers taken off the appOutputQ are put back
-     * into it until handshaking completes.
-     *
-     * This same method is called to try and resume output after a blocking
-     * handshaking operation has completed.
-     */
-    private boolean upperWrite(ByteBufferReference[] refs, AsyncWriteQueue delayCallback) {
-        // currently delayCallback is not used. Use it when it's needed to execute handshake in another thread.
-        try {
-            ByteBuffer[] buffers = ByteBufferReference.toBuffers(refs);
-            int bytes = Utils.remaining(buffers);
-            while (bytes > 0) {
-                EngineResult r = wrapBuffers(buffers);
-                int bytesProduced = r.bytesProduced();
-                int bytesConsumed = r.bytesConsumed();
-                bytes -= bytesConsumed;
-                if (bytesProduced > 0) {
-                    lowerOutput.writeAsync(new ByteBufferReference[]{r.destBuffer});
-                }
-
-                // The code below can be uncommented to shake out
-                // the implementation by inserting random delays and trigger
-                // handshake in the SelectorManager thread (upperRead)
-
-                // int sleep = random.nextInt(100);
-                // if (sleep > 20) {
-                //   Thread.sleep(sleep);
-                // }
-
-                // handshaking is happening or is needed
-                if (r.handshaking()) {
-                    Log.logTrace("Write: needs handshake");
-                    doHandshakeNow("Write");
-                }
-            }
-            ByteBufferReference.clear(refs);
-        } catch (Throwable t) {
-            closeExceptionally(t);
-            errorHandler.accept(t);
-        }
-        // We always return true: either all the data was sent, or
-        // an exception happened and we have closed the queue.
-        return true;
-    }
-
-    // Connecting at this level means the initial handshake has completed.
-    // This means that the initial SSL parameters are available including
-    // ALPN result.
-    void connect() throws IOException, InterruptedException {
-        doHandshakeNow("Init");
-        connected = true;
-    }
-
-    boolean connected() {
-        return connected;
-    }
-
-    private void startHandshake(String tag) {
-        Runnable run = () -> {
-            try {
-                doHandshakeNow(tag);
-            } catch (Throwable t) {
-                Log.logTrace("{0}: handshake failed: {1}", tag, t);
-                closeExceptionally(t);
-                errorHandler.accept(t);
-            }
-        };
-        client.executor().execute(run);
-    }
-
-    private void doHandshakeNow(String tag)
-        throws IOException, InterruptedException
-    {
-        handshaker.acquire();
-        try {
-            channelInputQ.disableCallback();
-            lowerOutput.flushAsync();
-            Log.logTrace("{0}: Starting handshake...", tag);
-            doHandshakeImpl();
-            Log.logTrace("{0}: Handshake completed", tag);
-            // don't unblock the channel here, as we aren't sure yet, whether ALPN
-            // negotiation succeeded. Caller will call enableCallback() externally
-        } finally {
-            handshaker.release();
-        }
-    }
-
-    public void enableCallback() {
-        channelInputQ.enableCallback();
-    }
-
-     /**
-     * Executes entire handshake in calling thread.
-     * Returns after handshake is completed or error occurs
-     */
-    private void doHandshakeImpl() throws IOException {
-        engine.beginHandshake();
-        while (true) {
-            SSLEngineResult.HandshakeStatus status = engine.getHandshakeStatus();
-            switch(status) {
-                case NEED_TASK: {
-                    List<Runnable> tasks = obtainTasks();
-                    for (Runnable task : tasks) {
-                        task.run();
-                    }
-                } break;
-                case NEED_WRAP:
-                    handshakeWrapAndSend();
-                    break;
-                case NEED_UNWRAP: case NEED_UNWRAP_AGAIN:
-                    handshakeReceiveAndUnWrap();
-                    break;
-                case FINISHED:
-                    return;
-                case NOT_HANDSHAKING:
-                    return;
-                default:
-                    throw new InternalError("Unexpected Handshake Status: "
-                                             + status);
-            }
-        }
-    }
-
-    // acknowledge a received CLOSE request from peer
-    void doClosure() throws IOException {
-        //while (!wrapAndSend(emptyArray))
-            //;
-    }
-
-    List<Runnable> obtainTasks() {
-        List<Runnable> l = new ArrayList<>();
-        Runnable r;
-        while ((r = engine.getDelegatedTask()) != null) {
-            l.add(r);
-        }
-        return l;
-    }
-
-    @Override
-    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-                                  Consumer<Throwable> errorReceiver,
-                                  Supplier<ByteBufferReference> readBufferSupplier) {
-        this.asyncReceiver = asyncReceiver;
-        this.errorHandler = errorReceiver;
-        // readBufferSupplier is not used,
-        // because of AsyncSSLDelegate has its own appBufferPool
-    }
-
-    @Override
-    public void startReading() {
-        // maybe this class does not need to implement AsyncConnection
-    }
-
-    @Override
-    public void stopAsyncReading() {
-        // maybe this class does not need to implement AsyncConnection
-    }
-
-
-    static class EngineResult {
-        final SSLEngineResult result;
-        final ByteBufferReference destBuffer;
-
-
-        // normal result
-        EngineResult(SSLEngineResult result) {
-            this(result, null);
-        }
-
-        EngineResult(SSLEngineResult result, ByteBufferReference destBuffer) {
-            this.result = result;
-            this.destBuffer = destBuffer;
-        }
-
-        boolean handshaking() {
-            SSLEngineResult.HandshakeStatus s = result.getHandshakeStatus();
-            return s != FINISHED && s != NOT_HANDSHAKING;
-        }
-
-        int bytesConsumed() {
-            return result.bytesConsumed();
-        }
-
-        int bytesProduced() {
-            return result.bytesProduced();
-        }
-
-        SSLEngineResult.HandshakeStatus handshakeStatus() {
-            return result.getHandshakeStatus();
-        }
-
-        SSLEngineResult.Status status() {
-            return result.getStatus();
-        }
-    }
-
-    EngineResult handshakeWrapAndSend() throws IOException {
-        EngineResult r = wrapBuffer(Utils.EMPTY_BYTEBUFFER);
-        if (r.bytesProduced() > 0) {
-            lowerOutput.writeAsync(new ByteBufferReference[]{r.destBuffer});
-            lowerOutput.flushAsync();
-        }
-        return r;
-    }
-
-    // called during handshaking. It blocks until a complete packet
-    // is available, unwraps it and returns.
-    void handshakeReceiveAndUnWrap() throws IOException {
-        ByteBufferReference ref = channelInputQ.take();
-        while (true) {
-            // block waiting for input
-            EngineResult r = unwrapBuffer(ref.get());
-            SSLEngineResult.Status status = r.status();
-            if (status == BUFFER_UNDERFLOW) {
-                // wait for another buffer to arrive
-                ByteBufferReference ref1 = channelInputQ.take();
-                ref = combine (ref, ref1);
-                continue;
-            }
-            // OK
-            // theoretically possible we could receive some user data
-            if (r.bytesProduced() > 0) {
-                asyncReceiver.accept(r.destBuffer);
-            } else {
-                r.destBuffer.clear();
-            }
-            // it is also possible that a delegated task could be needed
-            // even though they are handled in the calling function
-            if (r.handshakeStatus() == NEED_TASK) {
-                obtainTasks().stream().forEach((task) -> task.run());
-            }
-
-            if (!ref.get().hasRemaining()) {
-                ref.clear();
-                return;
-            }
-        }
-    }
-
-    EngineResult wrapBuffer(ByteBuffer src) throws SSLException {
-        ByteBuffer[] bufs = new ByteBuffer[1];
-        bufs[0] = src;
-        return wrapBuffers(bufs);
-    }
-
-    private final ByteBufferPool netBufferPool = new ByteBufferPool();
-    private final ByteBufferPool appBufferPool = new ByteBufferPool();
-
-    /**
-     * provides buffer of sslEngine@getPacketBufferSize().
-     * used for encrypted buffers after wrap or before unwrap.
-     * @return ByteBufferReference
-     */
-    public ByteBufferReference getNetBuffer() {
-        return netBufferPool.get(engine.getSession().getPacketBufferSize());
-    }
-
-    /**
-     * provides buffer of sslEngine@getApplicationBufferSize().
-     * @return ByteBufferReference
-     */
-    private ByteBufferReference getAppBuffer() {
-        return appBufferPool.get(engine.getSession().getApplicationBufferSize());
-    }
-
-    EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
-        ByteBufferReference dst = getNetBuffer();
-        while (true) {
-            SSLEngineResult sslResult = engine.wrap(src, dst.get());
-            switch (sslResult.getStatus()) {
-                case BUFFER_OVERFLOW:
-                    // Shouldn't happen. We allocated buffer with packet size
-                    // get it again if net buffer size was changed
-                    dst = getNetBuffer();
-                    break;
-                case CLOSED:
-                case OK:
-                    dst.get().flip();
-                    return new EngineResult(sslResult, dst);
-                case BUFFER_UNDERFLOW:
-                    // Shouldn't happen.  Doesn't returns when wrap()
-                    // underflow handled externally
-                    return new EngineResult(sslResult);
-                default:
-                    assert false;
-            }
-        }
-    }
-
-    EngineResult unwrapBuffer(ByteBuffer srcbuf) throws IOException {
-        ByteBufferReference dst = getAppBuffer();
-        while (true) {
-            SSLEngineResult sslResult = engine.unwrap(srcbuf, dst.get());
-            switch (sslResult.getStatus()) {
-                case BUFFER_OVERFLOW:
-                    // may happen only if app size buffer was changed.
-                    // get it again if app buffer size changed
-                    dst = getAppBuffer();
-                    break;
-                case CLOSED:
-                    doClosure();
-                    throw new IOException("Engine closed");
-                case BUFFER_UNDERFLOW:
-                    dst.clear();
-                    return new EngineResult(sslResult);
-                case OK:
-                     dst.get().flip();
-                     return new EngineResult(sslResult, dst);
-            }
-        }
-    }
-
-    /**
-     * Asynchronous read input. Call this when selector fires.
-     * Unwrap done in upperRead because it also happens in
-     * doHandshake() when handshake taking place
-     */
-    public void asyncReceive(ByteBufferReference buffer) {
-        try {
-            channelInputQ.put(buffer);
-        } catch (Throwable t) {
-            closeExceptionally(t);
-            errorHandler.accept(t);
-        }
-    }
-
-    private ByteBufferReference pollInput() throws IOException {
-        return channelInputQ.poll();
-    }
-
-    private ByteBufferReference pollInput(ByteBufferReference next) throws IOException {
-        return next == null ? channelInputQ.poll() : next;
-    }
-
-    public void upperRead() {
-        ByteBufferReference src;
-        ByteBufferReference next = null;
-        synchronized (reader) {
-            try {
-                src = pollInput();
-                if (src == null) {
-                    return;
-                }
-                while (true) {
-                    EngineResult r = unwrapBuffer(src.get());
-                    switch (r.result.getStatus()) {
-                        case BUFFER_UNDERFLOW:
-                            // Buffer too small. Need to combine with next buf
-                            next = pollInput(next);
-                            if (next == null) {
-                                // no data available.
-                                // push buffer back until more data available
-                                channelInputQ.pushback(src);
-                                return;
-                            } else {
-                                src = shift(src, next);
-                                if (!next.get().hasRemaining()) {
-                                    next.clear();
-                                    next = null;
-                                }
-                            }
-                            break;
-                        case OK:
-                            // check for any handshaking work
-                            if (r.handshaking()) {
-                                // handshaking is happening or is needed
-                                // so we put the buffer back on Q to process again
-                                // later.
-                                Log.logTrace("Read: needs handshake");
-                                channelInputQ.pushback(src);
-                                startHandshake("Read");
-                                return;
-                            }
-                            asyncReceiver.accept(r.destBuffer);
-                    }
-                    if (src.get().hasRemaining()) {
-                        continue;
-                    }
-                    src.clear();
-                    src = pollInput(next);
-                    next = null;
-                    if (src == null) {
-                        return;
-                    }
-                }
-            } catch (Throwable t) {
-                closeExceptionally(t);
-                errorHandler.accept(t);
-            }
-        }
-    }
-
-    ByteBufferReference shift(ByteBufferReference ref1, ByteBufferReference ref2) {
-        ByteBuffer buf1 = ref1.get();
-        if (buf1.capacity() < engine.getSession().getPacketBufferSize()) {
-            ByteBufferReference newRef = getNetBuffer();
-            ByteBuffer newBuf = newRef.get();
-            newBuf.put(buf1);
-            buf1 = newBuf;
-            ref1.clear();
-            ref1 = newRef;
-        } else {
-            buf1.compact();
-        }
-        ByteBuffer buf2 = ref2.get();
-        Utils.copy(buf2, buf1, Math.min(buf1.remaining(), buf2.remaining()));
-        buf1.flip();
-        return ref1;
-    }
-
-
-    ByteBufferReference combine(ByteBufferReference ref1, ByteBufferReference ref2) {
-        ByteBuffer buf1 = ref1.get();
-        ByteBuffer buf2 = ref2.get();
-        int avail1 = buf1.capacity() - buf1.remaining();
-        if (buf2.remaining() < avail1) {
-            buf1.compact();
-            buf1.put(buf2);
-            buf1.flip();
-            ref2.clear();
-            return ref1;
-        }
-        int newsize = buf1.remaining() + buf2.remaining();
-        ByteBuffer newbuf = ByteBuffer.allocate(newsize); // getting rid of buffer pools
-        newbuf.put(buf1);
-        newbuf.put(buf2);
-        newbuf.flip();
-        ref1.clear();
-        ref2.clear();
-        return ByteBufferReference.of(newbuf);
-    }
-
-    SSLParameters getSSLParameters() {
-        return sslParameters;
-    }
-
-    static void logParams(SSLParameters p) {
-        if (!Log.ssl()) {
-            return;
-        }
-
-        if (p == null) {
-            Log.logSSL("SSLParameters: Null params");
-            return;
-        }
-
-        final StringBuilder sb = new StringBuilder("SSLParameters:");
-        final List<Object> params = new ArrayList<>();
-        if (p.getCipherSuites() != null) {
-            for (String cipher : p.getCipherSuites()) {
-                sb.append("\n    cipher: {")
-                  .append(params.size()).append("}");
-                params.add(cipher);
-            }
-        }
-
-        // SSLParameters.getApplicationProtocols() can't return null
-        // JDK 8 EXCL START
-        for (String approto : p.getApplicationProtocols()) {
-            sb.append("\n    application protocol: {")
-              .append(params.size()).append("}");
-            params.add(approto);
-        }
-        // JDK 8 EXCL END
-
-        if (p.getProtocols() != null) {
-            for (String protocol : p.getProtocols()) {
-                sb.append("\n    protocol: {")
-                  .append(params.size()).append("}");
-                params.add(protocol);
-            }
-        }
-
-        if (p.getServerNames() != null) {
-            for (SNIServerName sname : p.getServerNames()) {
-                 sb.append("\n    server name: {")
-                  .append(params.size()).append("}");
-                params.add(sname.toString());
-            }
-        }
-        sb.append('\n');
-
-        Log.logSSL(sb.toString(), params.toArray());
-    }
-
-    String getSessionInfo() {
-        StringBuilder sb = new StringBuilder();
-        String application = engine.getApplicationProtocol();
-        SSLSession sess = engine.getSession();
-        String cipher = sess.getCipherSuite();
-        String protocol = sess.getProtocol();
-        sb.append("Handshake complete alpn: ")
-          .append(application)
-          .append(", Cipher: ")
-          .append(cipher)
-          .append(", Protocol: ")
-          .append(protocol);
-        return sb.toString();
-    }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,15 +26,12 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
 import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.SSLTube;
 import jdk.incubator.http.internal.common.Utils;
 
 /**
@@ -43,48 +40,43 @@
 class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection {
 
     final PlainTunnelingConnection plainConnection;
-    final AsyncSSLDelegate sslDelegate;
-    final String serverName;
+    final PlainHttpPublisher writePublisher;
+    volatile SSLTube flow;
 
-    @Override
-    public void connect() throws IOException, InterruptedException {
-        plainConnection.connect();
-        configureMode(Mode.ASYNC);
-        startReading();
-        sslDelegate.connect();
-    }
-
-    @Override
-    boolean connected() {
-        return plainConnection.connected() && sslDelegate.connected();
+    AsyncSSLTunnelConnection(InetSocketAddress addr,
+                             HttpClientImpl client,
+                             String[] alpn,
+                             InetSocketAddress proxy)
+    {
+        super(addr, client, Utils.getServerName(addr), alpn);
+        this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
+        this.writePublisher = new PlainHttpPublisher();
     }
 
     @Override
     public CompletableFuture<Void> connectAsync() {
-        throw new InternalError();
-    }
-
-    AsyncSSLTunnelConnection(InetSocketAddress addr,
-                        HttpClientImpl client,
-                        String[] alpn,
-                        InetSocketAddress proxy)
-    {
-        super(addr, client);
-        this.serverName = Utils.getServerName(addr);
-        this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
-        this.sslDelegate = new AsyncSSLDelegate(plainConnection, client, alpn, serverName);
+        debug.log(Level.DEBUG, "Connecting plain tunnel connection");
+        // This will connect the PlainHttpConnection flow, so that
+        // its HttpSubscriber and HttpPublisher are subscribed to the
+        // SocketTube
+        return plainConnection
+                .connectAsync()
+                .thenApply( unused -> {
+                    debug.log(Level.DEBUG, "creating SSLTube");
+                    // create the SSLTube wrapping the SocketTube, with the given engine
+                    flow = new SSLTube(engine,
+                                       client().theExecutor(),
+                                       plainConnection.getConnectionFlow());
+                    return null;} );
     }
 
     @Override
-    synchronized void configureMode(Mode mode) throws IOException {
-        super.configureMode(mode);
-        plainConnection.configureMode(mode);
+    boolean connected() {
+        return plainConnection.connected(); // && sslDelegate.connected();
     }
 
     @Override
-    SSLParameters sslParameters() {
-        return sslDelegate.getSSLParameters();
-    }
+    HttpPublisher publisher() { return writePublisher; }
 
     @Override
     public String toString() {
@@ -97,52 +89,28 @@
     }
 
     @Override
-    AsyncSSLDelegate sslDelegate() {
-        return sslDelegate;
-    }
-
-    @Override
     ConnectionPool.CacheKey cacheKey() {
         return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
     }
 
     @Override
-    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-        //debugPrint("Send", buffers, start, number);
-        ByteBuffer[] bufs = Utils.reduce(buffers, start, number);
-        long n = Utils.remaining(bufs);
-        sslDelegate.writeAsync(ByteBufferReference.toReferences(bufs));
-        sslDelegate.flushAsync();
-        return n;
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        //debugPrint("Send", buffer);
-        long n = buffer.remaining();
-        sslDelegate.writeAsync(ByteBufferReference.toReferences(buffer));
-        sslDelegate.flushAsync();
-        return n;
-    }
-
-    @Override
     public void writeAsync(ByteBufferReference[] buffers) throws IOException {
-        sslDelegate.writeAsync(buffers);
+        writePublisher.writeAsync(buffers);
     }
 
     @Override
     public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        sslDelegate.writeAsyncUnordered(buffers);
+        writePublisher.writeAsyncUnordered(buffers);
     }
 
     @Override
     public void flushAsync() throws IOException {
-        sslDelegate.flushAsync();
+        writePublisher.flushAsync();
     }
 
     @Override
     public void close() {
-        Utils.close(sslDelegate, plainConnection.channel());
+        plainConnection.close();
     }
 
     @Override
@@ -166,41 +134,13 @@
     }
 
     @Override
-    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-                                  Consumer<Throwable> errorReceiver,
-                                  Supplier<ByteBufferReference> readBufferSupplier) {
-        sslDelegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
-        plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
-    }
-
-    @Override
-    public void startReading() {
-        plainConnection.startReading();
-        sslDelegate.startReading();
-    }
-
-    @Override
-    public void stopAsyncReading() {
-        plainConnection.stopAsyncReading();
+    public void closeExceptionally(Throwable cause) {
+        debug.log(Level.DEBUG, "Closing connection: ", cause);
+        plainConnection.close();
     }
 
     @Override
-    public void enableCallback() {
-        sslDelegate.enableCallback();
-    }
-
-    @Override
-    public void closeExceptionally(Throwable cause) throws IOException {
-        Utils.close(cause, sslDelegate, plainConnection.channel());
-    }
-
-    @Override
-    SSLEngine getEngine() {
-        return sslDelegate.getEngine();
-    }
-
-    @Override
-    SSLTunnelConnection downgrade() {
-        return new SSLTunnelConnection(this);
-    }
+    SSLTube getConnectionFlow() {
+       return flow;
+   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncTriggerEvent.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * An asynchronous event which is triggered only once from the selector manager
+ * thread as soon as event registration are handled.
+ */
+final class AsyncTriggerEvent extends AsyncEvent{
+
+    private final Runnable trigger;
+    private final Consumer<? super IOException> errorHandler;
+    AsyncTriggerEvent(Consumer<? super IOException> errorHandler,
+                      Runnable trigger) {
+        super(0);
+        this.trigger = Objects.requireNonNull(trigger);
+        this.errorHandler = Objects.requireNonNull(errorHandler);
+    }
+    /** Returns null */
+    @Override
+    public SelectableChannel channel() { return null; }
+    /** Returns 0 */
+    @Override
+    public int interestOps() { return 0; }
+    @Override
+    public void handle() { trigger.run(); }
+    @Override
+    public void abort(IOException ioe) { errorHandler.accept(ioe); }
+    @Override
+    public boolean repeating() { return false; }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/BufferingSubscriber.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Objects;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * A buffering BodySubscriber. When subscribed, accumulates ( buffers ) a given
+ * amount ( in bytes ) of a publisher's data before pushing it to a downstream
+ * subscriber.
+ */
+class BufferingSubscriber<T> implements HttpResponse.BodySubscriber<T>
+{
+    /** The downstream consumer of the data. */
+    private final HttpResponse.BodySubscriber<T> downstreamSubscriber;
+    /** The amount of data to be accumulate before pushing downstream. */
+    private final int bufferSize;
+
+    /** The subscription, created lazily. */
+    private volatile Flow.Subscription subscription;
+    /** The downstream subscription, created lazily. */
+    private volatile DownstreamSubscription downstreamSubscription;
+
+    /** Must be held when accessing the internal buffers. */
+    private final Object buffersLock = new Object();
+    /** The internal buffers holding the buffered data. */
+    private ArrayList<ByteBuffer> internalBuffers;
+    /** The actual accumulated remaining bytes in internalBuffers. */
+    private int accumulatedBytes;
+
+    /** State of the buffering subscriber:
+     *  1) [UNSUBSCRIBED] when initially created
+     *  2) [ACTIVE] when subscribed and can receive data
+     *  3) [ERROR | CANCELLED | COMPLETE] (terminal state)
+     */
+    static final int UNSUBSCRIBED = 0x01;
+    static final int ACTIVE       = 0x02;
+    static final int ERROR        = 0x04;
+    static final int CANCELLED    = 0x08;
+    static final int COMPLETE     = 0x10;
+
+    private volatile int state;
+
+    BufferingSubscriber(HttpResponse.BodySubscriber<T> downstreamSubscriber,
+                        int bufferSize) {
+        this.downstreamSubscriber = downstreamSubscriber;
+        this.bufferSize = bufferSize;
+        synchronized (buffersLock) {
+            internalBuffers = new ArrayList<>();
+        }
+        state = UNSUBSCRIBED;
+    }
+
+    /** Returns the number of bytes remaining in the given buffers. */
+    private static final int remaining(List<ByteBuffer> buffers) {
+        return buffers.stream().mapToInt(ByteBuffer::remaining).sum();
+    }
+
+    /**
+     * Tells whether, or not, there is at least a sufficient number of bytes
+     * accumulated in the internal buffers. If the subscriber is COMPLETE, and
+     * has some buffered data, then there is always enough ( to pass downstream ).
+     */
+    private final boolean hasEnoughAccumulatedBytes() {
+        assert Thread.holdsLock(buffersLock);
+        return accumulatedBytes >= bufferSize
+                || (state == COMPLETE && accumulatedBytes > 0);
+    }
+
+    /**
+     * Returns a new, unmodifiable, List<ByteBuffer> containing exactly the
+     * amount of data as required before pushing downstream. The amount of data
+     * may be less than required ( bufferSize ), in the case where the subscriber
+     * is COMPLETE.
+     */
+    private List<ByteBuffer> fromInternalBuffers() {
+        assert Thread.holdsLock(buffersLock);
+        int leftToFill = bufferSize;
+        int state = this.state;
+        assert (state == ACTIVE || state == CANCELLED)
+                ? accumulatedBytes >= leftToFill : true;
+        List<ByteBuffer> dsts = new ArrayList<>();
+
+        ListIterator<ByteBuffer> itr = internalBuffers.listIterator();
+        while (itr.hasNext()) {
+            ByteBuffer b = itr.next();
+            if (b.remaining() <= leftToFill) {
+                itr.remove();
+                if (b.position() != 0)
+                    b = b.slice();  // ensure position = 0 when propagated
+                dsts.add(b);
+                leftToFill -= b.remaining();
+                accumulatedBytes -= b.remaining();
+                if (leftToFill == 0)
+                    break;
+            } else {
+                int prevLimit = b.limit();
+                b.limit(b.position() + leftToFill);
+                ByteBuffer slice = b.slice();
+                dsts.add(slice);
+                b.limit(prevLimit);
+                b.position(b.position() + leftToFill);
+                accumulatedBytes -= leftToFill;
+                leftToFill = 0;
+                break;
+            }
+        }
+        assert (state == ACTIVE || state == CANCELLED)
+                ? leftToFill == 0 : state == COMPLETE;
+        assert (state == ACTIVE || state == CANCELLED)
+                ? remaining(dsts) == bufferSize : state == COMPLETE;
+        assert accumulatedBytes >= 0;
+        assert dsts.stream().noneMatch(b -> b.position() != 0);
+        return Collections.unmodifiableList(dsts);
+    }
+
+    /** Subscription that is passed to the downstream subscriber. */
+    private class DownstreamSubscription implements Flow.Subscription {
+        private final AtomicBoolean cancelled = new AtomicBoolean(); // false
+        private final Demand demand = new Demand();
+
+        @Override
+        public void request(long n) {
+            if (n <= 0L) {
+                onError(new IllegalArgumentException(
+                        "non-positive subscription request"));
+                return;
+            }
+            if (cancelled.get())
+                return;
+
+            demand.increase(n);
+
+            pushDemanded();
+        }
+
+        private final SequentialScheduler pushDemandedScheduler =
+                new SequentialScheduler(new PushDemandedTask());
+
+        void pushDemanded() {
+            if (cancelled.get())
+                return;
+            pushDemandedScheduler.runOrSchedule();
+        }
+
+        class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask {
+            @Override
+            public void run() {
+                try {
+                    while (true) {
+                        List<ByteBuffer> item;
+                        synchronized (buffersLock) {
+                            if (cancelled.get())
+                                return;
+                            if (!hasEnoughAccumulatedBytes())
+                                break;
+                            if (!demand.tryDecrement())
+                                break;
+                            item = fromInternalBuffers();
+                        }
+                        assert item != null;
+
+                        downstreamSubscriber.onNext(item);
+                    }
+                    if (cancelled.get())
+                        return;
+
+                    // complete only if all data consumed
+                    boolean complete;
+                    synchronized (buffersLock) {
+                        complete = state == COMPLETE && internalBuffers.isEmpty();
+                    }
+                    if (complete) {
+                        downstreamSubscriber.onComplete();
+                        return;
+                    }
+                } catch (Throwable t) {
+                    cancel();  // cancel if there is any find of error
+                    throw t;
+                }
+
+                boolean requestMore = false;
+                synchronized (buffersLock) {
+                    if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) {
+                        // request more upstream data
+                        requestMore = true;
+                    }
+                }
+                if (requestMore)
+                    subscription.request(1);
+            }
+        }
+
+        @Override
+        public void cancel() {
+            if (cancelled.compareAndExchange(false, true))
+                return;  // already cancelled
+
+            state = CANCELLED;  // set CANCELLED state of upstream subscriber
+            subscription.cancel();  // cancel upstream subscription
+            pushDemandedScheduler.stop(); // stop the demand scheduler
+        }
+    }
+
+    @Override
+    public void onSubscribe(Flow.Subscription subscription) {
+        Objects.requireNonNull(subscription);
+        if (this.subscription != null) {
+            subscription.cancel();
+            return;
+        }
+
+        int s = this.state;
+        assert s == UNSUBSCRIBED;
+        state = ACTIVE;
+        this.subscription = subscription;
+        downstreamSubscription = new DownstreamSubscription();
+        downstreamSubscriber.onSubscribe(downstreamSubscription);
+    }
+
+    @Override
+    public void onNext(List<ByteBuffer> item) {
+        Objects.requireNonNull(item);
+
+        int s = state;
+        if (s == CANCELLED)
+            return;
+
+        if (s != ACTIVE)
+            throw new InternalError("onNext on inactive subscriber");
+
+        synchronized (buffersLock) {
+            accumulatedBytes += Utils.accumulateBuffers(internalBuffers, item);
+        }
+
+        downstreamSubscription.pushDemanded();
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+        Objects.requireNonNull(throwable);
+        int s = state;
+        assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+        state = ERROR;
+        downstreamSubscriber.onError(throwable);
+    }
+
+    @Override
+    public void onComplete() {
+        int s = state;
+        assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+        state = COMPLETE;
+        downstreamSubscription.pushDemanded();
+    }
+
+    @Override
+    public CompletionStage<T> getBody() {
+        return downstreamSubscriber.getBody();
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,14 +25,24 @@
 
 package jdk.incubator.http;
 
-import java.lang.ref.WeakReference;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.ListIterator;
 import java.util.Objects;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Optional;
+import java.util.concurrent.Flow;
+import java.util.stream.Collectors;
+import jdk.incubator.http.internal.common.FlowTube;
 import jdk.incubator.http.internal.common.Utils;
 
 /**
@@ -40,34 +50,18 @@
  */
 final class ConnectionPool {
 
-    // These counters are used to distribute ids for debugging
-    // The ACTIVE_CLEANER_COUNTER will tell how many CacheCleaner
-    // are active at a given time. It will increase when a new
-    // CacheCleaner is started and decrease when it exits.
-    static final AtomicLong ACTIVE_CLEANER_COUNTER = new AtomicLong();
-    // The POOL_IDS_COUNTER increases each time a new ConnectionPool
-    // is created. It may wrap and become negative but will never be
-    // decremented.
-    static final AtomicLong POOL_IDS_COUNTER = new AtomicLong();
-    // The cleanerCounter is used to name cleaner threads within a
-    // a connection pool, and increments monotically.
-    // It may wrap and become negative but will never be
-    // decremented.
-    final AtomicLong cleanerCounter = new AtomicLong();
-
     static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
             "jdk.httpclient.keepalive.timeout", 1200); // seconds
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
 
     // Pools of idle connections
 
-    final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
-    final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
-    // A monotically increasing id for this connection pool.
-    // It may be negative (that's OK)
-    // Mostly used for debugging purposes when looking at thread dumps.
-    // Global scope.
-    final long poolID = POOL_IDS_COUNTER.incrementAndGet();
-    final AtomicReference<CacheCleaner> cleanerRef;
+    private final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
+    private final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
+    private final ExpiryList expiryList;
+    private final String dbgTag; // used for debug
+    boolean stopped;
 
     /**
      * Entries in connection pool are keyed by destination address and/or
@@ -110,28 +104,30 @@
         }
     }
 
-    static class ExpiryEntry {
-        final HttpConnection connection;
-        final long expiry; // absolute time in seconds of expiry time
-        ExpiryEntry(HttpConnection connection, long expiry) {
-            this.connection = connection;
-            this.expiry = expiry;
-        }
+    ConnectionPool() {
+        this("ConnectionPool(?)");
     }
 
-    final LinkedList<ExpiryEntry> expiryList;
+    ConnectionPool(long clientId) {
+        this("ConnectionPool("+clientId+")");
+    }
 
     /**
      * There should be one of these per HttpClient.
      */
-    ConnectionPool() {
+    private ConnectionPool(String tag) {
+        dbgTag = tag;
         plainPool = new HashMap<>();
         sslPool = new HashMap<>();
-        expiryList = new LinkedList<>();
-        cleanerRef = new AtomicReference<>();
+        expiryList = new ExpiryList();
+    }
+
+    final String dbgString() {
+        return dbgTag;
     }
 
     void start() {
+        assert !stopped : "Already stopped";
     }
 
     static CacheKey cacheKey(InetSocketAddress destination,
@@ -143,6 +139,7 @@
     synchronized HttpConnection getConnection(boolean secure,
                                               InetSocketAddress addr,
                                               InetSocketAddress proxy) {
+        if (stopped) return null;
         CacheKey key = new CacheKey(addr, proxy);
         HttpConnection c = secure ? findConnection(key, sslPool)
                                   : findConnection(key, plainPool);
@@ -153,16 +150,49 @@
     /**
      * Returns the connection to the pool.
      */
-    synchronized void returnToPool(HttpConnection conn) {
-        if (conn instanceof PlainHttpConnection) {
-            putConnection(conn, plainPool);
-        } else {
-            putConnection(conn, sslPool);
+    void returnToPool(HttpConnection conn) {
+        returnToPool(conn, Instant.now(), KEEP_ALIVE);
+    }
+
+    // Called also by whitebox tests
+    void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
+
+        // Don't call registerCleanupTrigger while holding a lock,
+        // but register it before the connection is added to the pool,
+        // since we don't want to trigger the cleanup if the connection
+        // is not in the pool.
+        CleanupTrigger cleanup = registerCleanupTrigger(conn);
+
+        // it's possible that cleanup may have been called.
+        synchronized(this) {
+            if (cleanup.isDone()) {
+                return;
+            } else if (stopped) {
+                conn.close();
+                return;
+            }
+            if (conn instanceof PlainHttpConnection) {
+                putConnection(conn, plainPool);
+            } else {
+                assert conn.isSecure();
+                putConnection(conn, sslPool);
+            }
+            expiryList.add(conn, now, keepAlive);
         }
-        addToExpiryList(conn);
         //System.out.println("Return to pool: " + conn);
     }
 
+    private CleanupTrigger registerCleanupTrigger(HttpConnection conn) {
+        // Connect the connection flow to a pub/sub pair that will take the
+        // connection out of the pool and close it if anything happens
+        // while the connection is sitting in the pool.
+        CleanupTrigger cleanup = new CleanupTrigger(conn);
+        FlowTube flow = conn.getConnectionFlow();
+        debug.log(Level.DEBUG, "registering %s", cleanup);
+        flow.connectFlows(cleanup, cleanup);
+        return cleanup;
+    }
+
     private HttpConnection
     findConnection(CacheKey key,
                    HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
@@ -171,20 +201,24 @@
             return null;
         } else {
             HttpConnection c = l.removeFirst();
-            removeFromExpiryList(c);
+            expiryList.remove(c);
             return c;
         }
     }
 
     /* called from cache cleaner only  */
-    private void
+    private boolean
     removeFromPool(HttpConnection c,
                    HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
         //System.out.println("cacheCleaner removing: " + c);
-        LinkedList<HttpConnection> l = pool.get(c.cacheKey());
-        assert l != null;
-        boolean wasPresent = l.remove(c);
-        assert wasPresent;
+        assert Thread.holdsLock(this);
+        CacheKey k = c.cacheKey();
+        List<HttpConnection> l = pool.get(k);
+        if (l == null || l.isEmpty()) {
+            pool.remove(k);
+            return false;
+        }
+        return l.remove(c);
     }
 
     private void
@@ -199,135 +233,262 @@
         l.add(c);
     }
 
-    static String makeCleanerName(long poolId, long cleanerId) {
-        return "HTTP-Cache-cleaner-" + poolId + "-" + cleanerId;
+    /**
+     * Purge expired connection and return the number of milliseconds
+     * in which the next connection is scheduled to expire.
+     * If no connections are scheduled to be purged return 0.
+     * @return the delay in milliseconds in which the next connection will
+     *         expire.
+     */
+    long purgeExpiredConnectionsAndReturnNextDeadline() {
+        if (!expiryList.purgeMaybeRequired()) return 0;
+        return purgeExpiredConnectionsAndReturnNextDeadline(Instant.now());
     }
 
-    // only runs while entries exist in cache
-    final static class CacheCleaner extends Thread {
+    // Used for whitebox testing
+    long purgeExpiredConnectionsAndReturnNextDeadline(Instant now) {
+        long nextPurge = 0;
+
+        // We may be in the process of adding new elements
+        // to the expiry list - but those elements will not
+        // have outlast their keep alive timer yet since we're
+        // just adding them.
+        if (!expiryList.purgeMaybeRequired()) return nextPurge;
 
-        volatile boolean stopping;
-        // A monotically increasing id. May wrap and become negative (that's OK)
-        // Mostly used for debugging purposes when looking at thread dumps.
-        // Scoped per connection pool.
-        final long cleanerID;
-        // A reference to the owning ConnectionPool.
-        // This reference's referent may become null if the HttpClientImpl
-        // that owns this pool is GC'ed.
-        final WeakReference<ConnectionPool> ownerRef;
-
-        CacheCleaner(ConnectionPool owner) {
-            this(owner, owner.cleanerCounter.incrementAndGet());
+        List<HttpConnection> closelist;
+        synchronized (this) {
+            closelist = expiryList.purgeUntil(now);
+            for (HttpConnection c : closelist) {
+                if (c instanceof PlainHttpConnection) {
+                    boolean wasPresent = removeFromPool(c, plainPool);
+                    assert wasPresent;
+                } else {
+                    boolean wasPresent = removeFromPool(c, sslPool);
+                    assert wasPresent;
+                }
+            }
+            nextPurge = now.until(
+                    expiryList.nextExpiryDeadline().orElse(now),
+                    ChronoUnit.MILLIS);
         }
-
-        CacheCleaner(ConnectionPool owner, long cleanerID) {
-            super(null, null, makeCleanerName(owner.poolID, cleanerID), 0, false);
-            this.cleanerID = cleanerID;
-            this.ownerRef = new WeakReference<>(owner);
-            setDaemon(true);
-        }
+        closelist.forEach(this::close);
+        return nextPurge;
+    }
 
-        synchronized boolean stopping() {
-            return stopping || ownerRef.get() == null;
-        }
-
-        synchronized void stopCleaner() {
-            stopping = true;
-        }
+    private void close(HttpConnection c) {
+        try {
+            c.close();
+        } catch (Throwable e) {} // ignore
+    }
 
-        @Override
-        public void run() {
-            ACTIVE_CLEANER_COUNTER.incrementAndGet();
-            try {
-                while (!stopping()) {
-                    try {
-                        Thread.sleep(3000);
-                    } catch (InterruptedException e) {}
-                    ConnectionPool owner = ownerRef.get();
-                    if (owner == null) return;
-                    owner.cleanCache(this);
-                    owner = null;
-                }
-            } finally {
-                ACTIVE_CLEANER_COUNTER.decrementAndGet();
+    void stop() {
+        List<HttpConnection> closelist = Collections.emptyList();
+        try {
+            synchronized (this) {
+                stopped = true;
+                closelist = expiryList.stream()
+                    .map(e -> e.connection)
+                    .collect(Collectors.toList());
+                expiryList.clear();
+                plainPool.clear();
+                sslPool.clear();
             }
+        } finally {
+            closelist.forEach(this::close);
+        }
+    }
+
+    static final class ExpiryEntry {
+        final HttpConnection connection;
+        final Instant expiry; // absolute time in seconds of expiry time
+        ExpiryEntry(HttpConnection connection, Instant expiry) {
+            this.connection = connection;
+            this.expiry = expiry;
         }
     }
 
-    synchronized void removeFromExpiryList(HttpConnection c) {
-        if (c == null) {
-            return;
+    /**
+     * Manages a LinkedList of sorted ExpiryEntry. The entry with the closer
+     * deadline is at the tail of the list, and the entry with the farther
+     * deadline is at the head. In the most common situation, new elements
+     * will need to be added at the head (or close to it), and expired elements
+     * will need to be purged from the tail.
+     */
+    private static final class ExpiryList {
+        private final LinkedList<ExpiryEntry> list = new LinkedList<>();
+        private volatile boolean mayContainEntries;
+
+        // A loosely accurate boolean whose value is computed
+        // at the end of each operation performed on ExpiryList;
+        // Does not require synchronizing on the ConnectionPool.
+        boolean purgeMaybeRequired() {
+            return mayContainEntries;
+        }
+
+        // Returns the next expiry deadline
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool
+        Optional<Instant> nextExpiryDeadline() {
+            if (list.isEmpty()) return Optional.empty();
+            else return Optional.of(list.getLast().expiry);
+        }
+
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool
+        void add(HttpConnection conn) {
+            add(conn, Instant.now(), KEEP_ALIVE);
         }
-        ListIterator<ExpiryEntry> li = expiryList.listIterator();
-        while (li.hasNext()) {
-            ExpiryEntry e = li.next();
-            if (e.connection.equals(c)) {
-                li.remove();
-                return;
+
+        // Used by whitebox test.
+        void add(HttpConnection conn, Instant now, long keepAlive) {
+            Instant then = now.truncatedTo(ChronoUnit.SECONDS)
+                    .plus(keepAlive, ChronoUnit.SECONDS);
+
+            // Elements with the farther deadline are at the head of
+            // the list. It's more likely that the new element will
+            // have the farthest deadline, and will need to be inserted
+            // at the head of the list, so we're using an ascending
+            // list iterator to find the right insertion point.
+            ListIterator<ExpiryEntry> li = list.listIterator();
+            while (li.hasNext()) {
+                ExpiryEntry entry = li.next();
+
+                if (then.isAfter(entry.expiry)) {
+                    li.previous();
+                    // insert here
+                    li.add(new ExpiryEntry(conn, then));
+                    mayContainEntries = true;
+                    return;
+                }
+            }
+            // last (or first) element of list (the last element is
+            // the first when the list is empty)
+            list.add(new ExpiryEntry(conn, then));
+            mayContainEntries = true;
+        }
+
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool
+        void remove(HttpConnection c) {
+            if (c == null || list.isEmpty()) return;
+            ListIterator<ExpiryEntry> li = list.listIterator();
+            while (li.hasNext()) {
+                ExpiryEntry e = li.next();
+                if (e.connection.equals(c)) {
+                    li.remove();
+                    mayContainEntries = !list.isEmpty();
+                    return;
+                }
             }
         }
-        CacheCleaner cleaner = this.cleanerRef.get();
-        if (expiryList.isEmpty() && cleaner != null) {
-            this.cleanerRef.compareAndSet(cleaner, null);
-            cleaner.stopCleaner();
-            cleaner.interrupt();
-        }
-    }
+
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool.
+        // Purge all elements whose deadline is before now (now included).
+        List<HttpConnection> purgeUntil(Instant now) {
+            if (list.isEmpty()) return Collections.emptyList();
 
-    private void cleanCache(CacheCleaner cleaner) {
-        long now = System.currentTimeMillis() / 1000;
-        LinkedList<HttpConnection> closelist = new LinkedList<>();
+            List<HttpConnection> closelist = new ArrayList<>();
 
-        synchronized (this) {
-            ListIterator<ExpiryEntry> li = expiryList.listIterator();
+            // elements with the closest deadlines are at the tail
+            // of the queue, so we're going to use a descending iterator
+            // to remove them, and stop when we find the first element
+            // that has not expired yet.
+            Iterator<ExpiryEntry> li = list.descendingIterator();
             while (li.hasNext()) {
                 ExpiryEntry entry = li.next();
-                if (entry.expiry <= now) {
+                // use !isAfter instead of isBefore in order to
+                // remove the entry if its expiry == now
+                if (!entry.expiry.isAfter(now)) {
                     li.remove();
                     HttpConnection c = entry.connection;
                     closelist.add(c);
-                    if (c instanceof PlainHttpConnection) {
-                        removeFromPool(c, plainPool);
-                    } else {
-                        removeFromPool(c, sslPool);
-                    }
-                }
+                } else break; // the list is sorted
             }
-            if (expiryList.isEmpty() && cleaner != null) {
-                this.cleanerRef.compareAndSet(cleaner, null);
-                cleaner.stopCleaner();
-            }
+            mayContainEntries = !list.isEmpty();
+            return closelist;
         }
-        for (HttpConnection c : closelist) {
-            //System.out.println ("KAC: closing " + c);
-            c.close();
+
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool
+        java.util.stream.Stream<ExpiryEntry> stream() {
+            return list.stream();
+        }
+
+        // should only be called while holding a synchronization
+        // lock on the ConnectionPool
+        void clear() {
+            list.clear();
+            mayContainEntries = false;
         }
     }
 
-    private synchronized void addToExpiryList(HttpConnection conn) {
-        long now = System.currentTimeMillis() / 1000;
-        long then = now + KEEP_ALIVE;
-        if (expiryList.isEmpty()) {
-            CacheCleaner cleaner = new CacheCleaner(this);
-            if (this.cleanerRef.compareAndSet(null, cleaner)) {
-                cleaner.start();
+    void cleanup(HttpConnection c, Throwable error) {
+        debug.log(Level.DEBUG,
+                  "%s : ConnectionPool.cleanup(%s)",
+                  String.valueOf(c.getConnectionFlow()),
+                  error);
+        synchronized(this) {
+            if (c instanceof PlainHttpConnection) {
+                removeFromPool(c, plainPool);
+            } else {
+                assert c.isSecure();
+                removeFromPool(c, sslPool);
             }
-            expiryList.add(new ExpiryEntry(conn, then));
-            return;
+            expiryList.remove(c);
+        }
+        c.close();
+    }
+
+    /**
+     * An object that subscribes to the flow while the connection is in
+     * the pool. Anything that comes in will cause the connection to be closed
+     * and removed from the pool.
+     */
+    private final class CleanupTrigger implements
+            FlowTube.TubeSubscriber, FlowTube.TubePublisher,
+            Flow.Subscription {
+
+        private final HttpConnection connection;
+        private volatile boolean done;
+
+        public CleanupTrigger(HttpConnection connection) {
+            this.connection = connection;
         }
 
-        ListIterator<ExpiryEntry> li = expiryList.listIterator();
-        while (li.hasNext()) {
-            ExpiryEntry entry = li.next();
+        public boolean isDone() { return done;}
+
+        private void triggerCleanup(Throwable error) {
+            done = true;
+            cleanup(connection, error);
+        }
+
+        @Override public void request(long n) {}
+        @Override public void cancel() {}
 
-            if (then > entry.expiry) {
-                li.previous();
-                // insert here
-                li.add(new ExpiryEntry(conn, then));
-                return;
-            }
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            subscription.request(1);
+        }
+        @Override
+        public void onError(Throwable error) { triggerCleanup(error); }
+        @Override
+        public void onComplete() { triggerCleanup(null); }
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            triggerCleanup(new IOException("Data received while in pool"));
         }
-        // first element of list
-        expiryList.add(new ExpiryEntry(conn, then));
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+            subscriber.onSubscribe(this);
+        }
+
+        @Override
+        public String toString() {
+            return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
+        }
+
     }
+
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/DefaultPublisher.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/*
- * Copyright (c) 2016, 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.incubator.http;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Flow;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Supplier;
-
-class DefaultPublisher<T> implements Flow.Publisher<T> {
-
-    private final Supplier<Optional<T>> supplier;
-    // this executor will be wrapped in another executor
-    // which may override it and just run in the calling thread
-    // if it knows the user call is blocking
-    private final Executor executor;
-
-    /**
-     * Supplier returns non empty Optionals until final
-     */
-    DefaultPublisher(Supplier<Optional<T>> supplier, Executor executor) {
-        this.supplier = supplier;
-        this.executor = executor;
-    }
-
-    @Override
-    public void subscribe(Flow.Subscriber<? super T> subscriber) {
-        try {
-            subscriber.onSubscribe(new Subscription(subscriber));
-        } catch (RejectedExecutionException e) {
-            subscriber.onError(new IllegalStateException(e));
-        }
-    }
-
-    private class Subscription implements Flow.Subscription {
-
-        private final Flow.Subscriber<? super T> subscriber;
-        private final AtomicBoolean done = new AtomicBoolean();
-
-        private final AtomicLong demand = new AtomicLong();
-
-        private final Lock consumerLock = new ReentrantLock();
-        private final Condition consumerAlarm = consumerLock.newCondition();
-
-        Subscription(Flow.Subscriber<? super T> subscriber) {
-            this.subscriber = subscriber;
-
-            executor.execute(() -> {
-                try {
-                    while (!done.get()) {
-                        consumerLock.lock();
-                        try {
-                            while (!done.get() && demand.get() == 0) {
-                                consumerAlarm.await();
-                            }
-                        } finally {
-                            consumerLock.unlock();
-                        }
-
-                        long nbItemsDemanded = demand.getAndSet(0);
-                        for (long i = 0; i < nbItemsDemanded && !done.get(); i++) {
-                            try {
-                                Optional<T> item = Objects.requireNonNull(supplier.get());
-                                if (item.isPresent()) {
-                                    subscriber.onNext(item.get());
-                                } else {
-                                    if (done.compareAndSet(false, true)) {
-                                        subscriber.onComplete();
-                                    }
-                                }
-                            } catch (RuntimeException e) {
-                                if (done.compareAndSet(false, true)) {
-                                    subscriber.onError(e);
-                                }
-                            }
-                        }
-                    }
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    if (done.compareAndSet(false, true)) {
-                        subscriber.onError(e);
-                    }
-                }
-            });
-        }
-
-        @Override
-        public void request(long n) {
-            if (!done.get() && n > 0) {
-                demand.updateAndGet(d -> (d + n > 0) ? d + n : Long.MAX_VALUE);
-                wakeConsumer();
-            } else if (done.compareAndSet(false, true)) {
-                subscriber.onError(new IllegalArgumentException("request(" + n + ")"));
-            }
-        }
-
-        @Override
-        public void cancel() {
-            done.set(true);
-            wakeConsumer();
-        }
-
-        private void wakeConsumer() {
-            consumerLock.lock();
-            try {
-                consumerAlarm.signal();
-            } finally {
-                consumerLock.unlock();
-            }
-        }
-
-    }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,7 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.ProxySelector;
 import java.net.SocketPermission;
@@ -40,7 +40,10 @@
 import java.security.PrivilegedExceptionAction;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.Executor;
 import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
@@ -61,19 +64,22 @@
  */
 final class Exchange<T> {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
     final HttpRequestImpl request;
     final HttpClientImpl client;
     volatile ExchangeImpl<T> exchImpl;
     // used to record possible cancellation raised before the exchImpl
     // has been established.
     private volatile IOException failed;
-    final List<SocketPermission> permissions = new LinkedList<>();
     final AccessControlContext acc;
     final MultiExchange<?,T> multi;
     final Executor parentExecutor;
-    final HttpRequest.BodyProcessor requestProcessor;
+    final HttpRequest.BodyPublisher requestPublisher;
     boolean upgrading; // to HTTP/2
     final PushGroup<?,T> pushGroup;
+    final String dbgTag;
 
     Exchange(HttpRequestImpl request, MultiExchange<?,T> multi) {
         this.request = request;
@@ -82,8 +88,9 @@
         this.multi = multi;
         this.acc = multi.acc;
         this.parentExecutor = multi.executor;
-        this.requestProcessor = request.requestProcessor;
+        this.requestPublisher = request.requestPublisher;
         this.pushGroup = multi.pushGroup;
+        this.dbgTag = "Exchange";
     }
 
     /* If different AccessControlContext to be used  */
@@ -97,8 +104,9 @@
         this.client = multi.client();
         this.multi = multi;
         this.parentExecutor = multi.executor;
-        this.requestProcessor = request.requestProcessor;
+        this.requestPublisher = request.requestPublisher;
         this.pushGroup = multi.pushGroup;
+        this.dbgTag = "Exchange";
     }
 
     PushGroup<?,T> getPushGroup() {
@@ -117,18 +125,25 @@
         return client;
     }
 
-    public Response response() throws IOException, InterruptedException {
-        return responseImpl(null);
-    }
-
-    public T readBody(HttpResponse.BodyHandler<T> responseHandler) throws IOException {
-        // The connection will not be returned to the pool in the case of WebSocket
-        return exchImpl.readBody(responseHandler, !request.isWebSocket());
-    }
 
     public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
         // The connection will not be returned to the pool in the case of WebSocket
-        return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor);
+        return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
+                .whenComplete((r,t) -> exchImpl.completed());
+    }
+
+    /**
+     * Called when a new exchange is created to replace this exchange.
+     * At this point it is guaranteed that readBody/readBodyAsync will
+     * not be called.
+     */
+    public void released() {
+        ExchangeImpl<?> impl = exchImpl;
+        if (impl != null) impl.released();
+        // Don't set exchImpl to null here. We need to keep
+        // it alive until it's replaced by a Stream in wrapForUpgrade.
+        // Setting it to null here might get it GC'ed too early, because
+        // the Http1Response is now only weakly referenced by the Selector.
     }
 
     public void cancel() {
@@ -153,19 +168,15 @@
         ExchangeImpl<?> impl = exchImpl;
         if (impl != null) {
             // propagate the exception to the impl
+            debug.log(Level.DEBUG, "Cancelling exchImpl: %s", exchImpl);
             impl.cancel(cause);
         } else {
-            try {
-                // no impl yet. record the exception
-                failed = cause;
-                // 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.
-                checkCancelled(false);
-            } catch (IOException x) {
-                // should not happen - we passed 'false' above
-                throw new UncheckedIOException(x);
-            }
+            // no impl yet. record the exception
+            failed = cause;
+            // 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.
+            checkCancelled();
         }
     }
 
@@ -175,36 +186,30 @@
     // will persist until the exception can be raised and the failed state
     // can be cleared.
     // Takes care of possible race conditions.
-    private void checkCancelled(boolean throwIfNoImpl) throws IOException {
+    private void checkCancelled() {
         ExchangeImpl<?> impl = null;
         IOException cause = null;
         if (failed != null) {
             synchronized(this) {
                 cause = failed;
                 impl = exchImpl;
-                if (throwIfNoImpl || impl != null) {
-                    // The exception will be raised by one of the two methods
-                    // below: reset the failed state.
-                    failed = null;
-                }
             }
         }
         if (cause == null) return;
         if (impl != null) {
             // The exception is raised by propagating it to the impl.
+            debug.log(Level.DEBUG, "Cancelling exchImpl: %s", impl);
             impl.cancel(cause);
-        } else if (throwIfNoImpl) {
-            // The exception is raised by throwing it immediately
-            throw cause;
+            failed = null;
         } else {
             Log.logTrace("Exchange: request [{0}/timeout={1}ms] no impl is set."
                          + "\n\tCan''t cancel yet with {2}",
                          request.uri(),
-                         request.duration() == null ? -1 :
+                         request.timeout().isPresent() ?
                          // calling duration.toMillis() can throw an exception.
                          // this is just debugging, we don't care if it overflows.
-                         (request.duration().getSeconds() * 1000
-                          + request.duration().getNano() / 1000000),
+                         (request.timeout().get().getSeconds() * 1000
+                          + request.timeout().get().getNano() / 1000000) : -1,
                          cause);
         }
     }
@@ -216,90 +221,34 @@
 
     static final SocketPermission[] SOCKET_ARRAY = new SocketPermission[0];
 
-    Response responseImpl(HttpConnection connection)
-        throws IOException, InterruptedException
-    {
-        SecurityException e = securityCheck(acc);
-        if (e != null) {
-            throw e;
-        }
-
-        if (permissions.size() > 0) {
-            try {
-                return AccessController.doPrivileged(
-                        (PrivilegedExceptionAction<Response>)() ->
-                             responseImpl0(connection),
-                        null,
-                        permissions.toArray(SOCKET_ARRAY));
-            } catch (Throwable ee) {
-                if (ee instanceof PrivilegedActionException) {
-                    ee = ee.getCause();
-                }
-                if (ee instanceof IOException) {
-                    throw (IOException) ee;
-                } else {
-                    throw new RuntimeException(ee); // TODO: fix
-                }
-            }
-        } else {
-            return responseImpl0(connection);
-        }
+    synchronized IOException getCancelCause() {
+        return failed;
     }
 
     // get/set the exchange impl, solving race condition issues with
     // potential concurrent calls to cancel() or cancel(IOException)
-    private void establishExchange(HttpConnection connection)
-        throws IOException, InterruptedException
-    {
+    private CompletableFuture<? extends ExchangeImpl<T>>
+    establishExchange(HttpConnection connection) {
         // check if we have been cancelled first.
-        checkCancelled(true);
-        // not yet cancelled: create/get a new impl
-        exchImpl = ExchangeImpl.get(this, connection);
-        // recheck for cancelled, in case of race conditions
-        checkCancelled(true);
-        // now we're good to go. because exchImpl is no longer null
-        // cancel() will be able to propagate directly to the impl
-        // after this point.
-    }
-
-    private Response responseImpl0(HttpConnection connection)
-        throws IOException, InterruptedException
-    {
-        establishExchange(connection);
-        if (request.expectContinue()) {
-            Log.logTrace("Sending Expect: 100-Continue");
-            request.addSystemHeader("Expect", "100-Continue");
-            exchImpl.sendHeadersOnly();
-
-            Log.logTrace("Waiting for 407-Expectation-Failed or 100-Continue");
-            Response resp = exchImpl.getResponse();
-            HttpResponseImpl.logResponse(resp);
-            int rcode = resp.statusCode();
-            if (rcode != 100) {
-                Log.logTrace("Expectation failed: Received {0}",
-                             rcode);
-                if (upgrading && rcode == 101) {
-                    throw new IOException(
-                        "Unable to handle 101 while waiting for 100-Continue");
-                }
-                return resp;
-            }
-
-            Log.logTrace("Received 100-Continue: sending body");
-            exchImpl.sendBody();
-
-            Log.logTrace("Body sent: waiting for response");
-            resp = exchImpl.getResponse();
-            HttpResponseImpl.logResponse(resp);
-
-            return checkForUpgrade(resp, exchImpl);
-        } else {
-            exchImpl.sendHeadersOnly();
-            exchImpl.sendBody();
-            Response resp = exchImpl.getResponse();
-            HttpResponseImpl.logResponse(resp);
-            return checkForUpgrade(resp, exchImpl);
+        Throwable t = getCancelCause();
+        checkCancelled();
+        if (t != null) {
+            return MinimalFuture.failedFuture(t);
         }
+        CompletableFuture<? extends ExchangeImpl<T>> cf = ExchangeImpl.get(this, connection);
+        return cf.thenCompose((eimpl) -> {
+                    // recheck for cancelled, in case of race conditions
+                    exchImpl = eimpl;
+                    IOException tt = getCancelCause();
+                    checkCancelled();
+                    if (tt != null) {
+                        return MinimalFuture.failedFuture(tt);
+                    } else {
+                        // Now we're good to go. Because exchImpl is no longer
+                        // null cancel() will be able to propagate directly to
+                        // the impl after this point ( if needed ).
+                        return MinimalFuture.completedFuture(eimpl);
+                    } });
     }
 
     // Completed HttpResponse will be null if response succeeded
@@ -310,35 +259,23 @@
     }
 
     CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
-        SecurityException e = securityCheck(acc);
+        SecurityException e = checkPermissions();
         if (e != null) {
             return MinimalFuture.failedFuture(e);
-        }
-        if (permissions.size() > 0) {
-            return AccessController.doPrivileged(
-                    (PrivilegedAction<CompletableFuture<Response>>)() ->
-                        responseAsyncImpl0(connection),
-                    null,
-                    permissions.toArray(SOCKET_ARRAY));
         } else {
             return responseAsyncImpl0(connection);
         }
     }
 
     CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
-        try {
-            establishExchange(connection);
-        } catch (IOException | InterruptedException e) {
-            return MinimalFuture.failedFuture(e);
-        }
         if (request.expectContinue()) {
             request.addSystemHeader("Expect", "100-Continue");
             Log.logTrace("Sending Expect: 100-Continue");
-            return exchImpl
-                    .sendHeadersAsync()
+            return establishExchange(connection)
+                    .thenCompose((ex) -> ex.sendHeadersAsync())
                     .thenCompose(v -> exchImpl.getResponseAsync(parentExecutor))
                     .thenCompose((Response r1) -> {
-                        HttpResponseImpl.logResponse(r1);
+                        Log.logResponse(r1::toString);
                         int rcode = r1.statusCode();
                         if (rcode == 100) {
                             Log.logTrace("Received 100-Continue: sending body");
@@ -361,8 +298,8 @@
                         }
                     });
         } else {
-            CompletableFuture<Response> cf = exchImpl
-                    .sendHeadersAsync()
+            CompletableFuture<Response> cf = establishExchange(connection)
+                    .thenCompose((ex) -> ex.sendHeadersAsync())
                     .thenCompose(ExchangeImpl::sendBodyAsync)
                     .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
             cf = wrapForUpgrade(cf);
@@ -381,15 +318,15 @@
     private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
         if (Log.requests()) {
             return cf.thenApply(response -> {
-                HttpResponseImpl.logResponse(response);
+                Log.logResponse(response::toString);
                 return response;
             });
         }
         return cf;
     }
 
-    HttpResponse.BodyProcessor<T> ignoreBody(int status, HttpHeaders hdrs) {
-        return HttpResponse.BodyProcessor.discard((T)null);
+    HttpResponse.BodySubscriber<T> ignoreBody(int status, HttpHeaders hdrs) {
+        return HttpResponse.BodySubscriber.discard((T)null);
     }
 
     // if this response was received in reply to an upgrade
@@ -406,50 +343,59 @@
             // check for 101 switching protocols
             // 101 responses are not supposed to contain a body.
             //    => should we fail if there is one?
+            debug.log(Level.DEBUG, "Upgrading async %s" + e.connection());
             return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
-                .thenCompose((T v) -> // v is null
-                     Http2Connection.createAsync(e.connection(),
+                .thenCompose((T v) -> {// v is null
+                    debug.log(Level.DEBUG, "Ignored body");
+                    // we pass e::getBuffer to allow the ByteBuffers to accumulate
+                    // while we build the Http2Connection
+                    return Http2Connection.createAsync(e.connection(),
                                                  client.client2(),
-                                                 this, e.getBuffer())
+                                                 this, e::getBuffer)
                         .thenCompose((Http2Connection c) -> {
                             c.putConnection();
                             Stream<T> s = c.getStream(1);
-                            exchImpl = s;
+                            if (s == null) {
+                                // s can be null if an exception occurred
+                                // asynchronously while sending the preface.
+                                Throwable t = c.getRecordedCause();
+                                if (t != null) {
+                                    return MinimalFuture.failedFuture(
+                                            new IOException("Can't get stream 1: " + t, t));
+                                }
+                            }
+                            exchImpl.released();
+                            Throwable t;
+                            // There's a race condition window where an external
+                            // thread (SelectorManager) might complete the
+                            // exchange in timeout at the same time where we're
+                            // trying to switch the exchange impl.
+                            // 'failed' will be reset to null after
+                            // exchImpl.cancel() has completed, so either we
+                            // will observe failed != null here, or we will
+                            // observe e.getCancelCause() != null, or the
+                            // timeout exception will be routed to 's'.
+                            // Either way, we need to relay it to s.
+                            synchronized (this) {
+                                exchImpl = s;
+                                t = failed;
+                            }
+                            // Check whether the HTTP/1.1 was cancelled.
+                            if (t == null) t = e.getCancelCause();
+                            // if HTTP/1.1 exchange was timed out, don't
+                            // try to go further.
+                            if (t instanceof HttpTimeoutException) {
+                                 s.cancelImpl(t);
+                                 return MinimalFuture.failedFuture(t);
+                            }
+                            debug.log(Level.DEBUG, "Getting response async %s" + s);
                             return s.getResponseAsync(null);
-                        })
+                        });}
                 );
         }
         return MinimalFuture.completedFuture(resp);
     }
 
-    private Response checkForUpgrade(Response resp,
-                                             ExchangeImpl<T> ex)
-        throws IOException, InterruptedException
-    {
-        int rcode = resp.statusCode();
-        if (upgrading && (rcode == 101)) {
-            Http1Exchange<T> e = (Http1Exchange<T>) ex;
-
-            // 101 responses are not supposed to contain a body.
-            //    => should we fail if there is one?
-            //    => readBody called here by analogy with
-            //       checkForUpgradeAsync above
-            e.readBody(this::ignoreBody, false);
-
-            // must get connection from Http1Exchange
-            Http2Connection h2con = new Http2Connection(e.connection(),
-                                                        client.client2(),
-                                                        this, e.getBuffer());
-            h2con.putConnection();
-            Stream<T> s = h2con.getStream(1);
-            exchImpl = s;
-            Response xx = s.getResponse();
-            HttpResponseImpl.logResponse(xx);
-            return xx;
-        }
-        return resp;
-    }
-
     private URI getURIForSecurityCheck() {
         URI u;
         String method = request.method();
@@ -476,12 +422,50 @@
     }
 
     /**
-     * Do the security check and return any exception.
-     * Return null if no check needed or passes.
-     *
-     * Also adds any generated permissions to the "permissions" list.
+     * Returns the security permission required for the given details.
+     * If method is CONNECT, then uri must be of form "scheme://host:port"
      */
-    private SecurityException securityCheck(AccessControlContext acc) {
+    private static URLPermission getPermissionFor(URI uri,
+                                                  String method,
+                                                  Map<String, List<String>> headers) {
+        StringBuilder sb = new StringBuilder();
+
+        String urlstring, actionstring;
+
+        if (method.equals("CONNECT")) {
+            urlstring = uri.toString();
+            actionstring = "CONNECT";
+        } else {
+            sb.append(uri.getScheme()).append("://")
+              .append(uri.getAuthority())
+              .append(uri.getPath());
+            urlstring = sb.toString();
+
+            sb = new StringBuilder();
+            sb.append(method);
+            if (headers != null && !headers.isEmpty()) {
+                sb.append(':');
+                Set<String> keys = headers.keySet();
+                boolean first = true;
+                for (String key : keys) {
+                    if (!first) {
+                        sb.append(',');
+                    }
+                    sb.append(key);
+                    first = false;
+                }
+            }
+            actionstring = sb.toString();
+        }
+        return new URLPermission(urlstring, actionstring);
+    }
+
+    /**
+     * Performs the necessary security permission checks required to retrieve
+     * the response. Returns a security exception representing the denied
+     * permission, or null if all checks pass or there is no security manager.
+     */
+    private SecurityException checkPermissions() {
         SecurityManager sm = System.getSecurityManager();
         if (sm == null) {
             return null;
@@ -490,12 +474,11 @@
         String method = request.method();
         HttpHeaders userHeaders = request.getUserHeaders();
         URI u = getURIForSecurityCheck();
-        URLPermission p = Utils.getPermission(u, method, userHeaders.map());
+        URLPermission p = getPermissionFor(u, method, userHeaders.map());
 
         try {
             assert acc != null;
             sm.checkPermission(p, acc);
-            permissions.add(getSocketPermissionFor(u));
         } catch (SecurityException e) {
             return e;
         }
@@ -516,13 +499,8 @@
                 try {
                     sm.checkPermission(p, acc);
                 } catch (SecurityException e) {
-                    permissions.clear();
                     return e;
                 }
-                String sockperm = proxy.getHostString() +
-                        ":" + Integer.toString(proxy.getPort());
-
-                permissions.add(new SocketPermission(sockperm, "connect,resolve"));
             }
         }
         return null;
@@ -563,4 +541,8 @@
     AccessControlContext getAccessControlContext() {
         return acc;
     }
+
+    String dbgString() {
+        return dbgTag;
+    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,10 +26,13 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.function.Function;
 import jdk.incubator.http.internal.common.MinimalFuture;
 import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import jdk.incubator.http.internal.common.Utils;
 
 /**
  * Splits request so that headers and body can be sent separately with optional
@@ -46,6 +49,10 @@
  */
 abstract class ExchangeImpl<T> {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    private static final System.Logger DEBUG_LOGGER =
+            Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG);
+
     final Exchange<T> exchange;
 
     ExchangeImpl(Exchange<T> e) {
@@ -68,94 +75,125 @@
      * Initiates a new exchange and assigns it to a connection if one exists
      * already. connection usually null.
      */
-    static <U> ExchangeImpl<U> get(Exchange<U> exchange, HttpConnection connection)
-        throws IOException, InterruptedException
+    static <U> CompletableFuture<? extends ExchangeImpl<U>>
+    get(Exchange<U> exchange, HttpConnection connection)
     {
         HttpRequestImpl req = exchange.request();
         if (exchange.version() == HTTP_1_1) {
-            return new Http1Exchange<>(exchange, connection);
+            DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
+            return createHttp1Exchange(exchange, connection);
         } else {
             Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
             HttpRequestImpl request = exchange.request();
-            Http2Connection c;
-            try {
-                c = c2.getConnectionFor(request);
-            } catch (Http2Connection.ALPNException e) {
-                // failed to negotiate "h2"
-                AbstractAsyncSSLConnection as = e.getConnection();
-                as.stopAsyncReading();
-                HttpConnection sslc = as.downgrade();
-                ExchangeImpl<U> ex = new Http1Exchange<>(exchange, sslc);
+            CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
+            DEBUG_LOGGER.log(Level.DEBUG, "get: Trying to get HTTP/2 connection");
+            return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
+                    .thenCompose(Function.identity());
+        }
+    }
+
+    private static <U> CompletableFuture<? extends ExchangeImpl<U>>
+    createExchangeImpl(Http2Connection c,
+                       Throwable t,
+                       Exchange<U> exchange,
+                       HttpConnection connection)
+    {
+        DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
+        if (t != null) {
+            DEBUG_LOGGER.log(Level.DEBUG,
+                             "handling HTTP/2 connection creation failed: %s",
+                             (Object)t);
+            t = Utils.getCompletionCause(t);
+            if (t instanceof Http2Connection.ALPNException) {
+                Http2Connection.ALPNException ee = (Http2Connection.ALPNException)t;
+                AbstractAsyncSSLConnection as = ee.getConnection();
+                DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
+                CompletableFuture<? extends ExchangeImpl<U>> ex =
+                        createHttp1Exchange(exchange, as);
                 return ex;
+            } else {
+                DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
+                                  + "with unexpected exception: %s", (Object)t);
+                return CompletableFuture.failedFuture(t);
             }
-            if (c == null) {
-                // no existing connection. Send request with HTTP 1 and then
-                // upgrade if successful
-                ExchangeImpl<U> ex = new Http1Exchange<>(exchange, connection);
-                exchange.h2Upgrade();
-                return ex;
-            }
-            return c.createStream(exchange);
+        }
+        if (c == null) {
+            // no existing connection. Send request with HTTP 1 and then
+            // upgrade if successful
+            DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
+            return createHttp1Exchange(exchange, connection)
+                    .thenApply((e) -> {
+                        exchange.h2Upgrade();
+                        return e;
+                    });
+        } else {
+            DEBUG_LOGGER.log(Level.DEBUG, "creating HTTP/2 streams");
+            Stream<U> s = c.createStream(exchange);
+            CompletableFuture<? extends ExchangeImpl<U>> ex = MinimalFuture.completedFuture(s);
+            return ex;
+        }
+    }
+
+    private static <T> CompletableFuture<Http1Exchange<T>>
+    createHttp1Exchange(Exchange<T> ex, HttpConnection as)
+    {
+        try {
+            return MinimalFuture.completedFuture(new Http1Exchange<>(ex, as));
+        } catch (Throwable e) {
+            return MinimalFuture.failedFuture(e);
         }
     }
 
     /* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
 
-    /**
-     * Sends the request headers only. May block until all sent.
-     */
-    abstract void sendHeadersOnly() throws IOException, InterruptedException;
-
-    // Blocking impl but in async style
+    abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
 
-    CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
-        // this is blocking. cf will already be completed.
-        return MinimalFuture.supply(() -> {
-            sendHeadersOnly();
-            return this;
-        });
-    }
-
-    /**
-     * Gets response by blocking if necessary. This may be an
-     * intermediate response (like 101) or a final response 200 etc. Returns
-     * before body is read.
-     */
-    abstract Response getResponse() throws IOException;
-
-    abstract T readBody(HttpResponse.BodyHandler<T> handler,
-                        boolean returnConnectionToPool) throws IOException;
+    /** Sends a request body, after request headers have been sent. */
+    abstract CompletableFuture<ExchangeImpl<T>> sendBodyAsync();
 
     abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
                                                 boolean returnConnectionToPool,
                                                 Executor executor);
 
-    /**
-     * Async version of getResponse. Completes before body is read.
-     */
+    /** Gets the response headers. Completes before body is read. */
     abstract CompletableFuture<Response> getResponseAsync(Executor executor);
 
-    /**
-     * Sends a request body after request headers.
-     */
-    abstract void sendBody() throws IOException, InterruptedException;
 
-    // Async version of sendBody(). This only used when body sent separately
-    // to headers (100 continue)
-    CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
-        return MinimalFuture.supply(() -> {
-            sendBody();
-            return this;
-        });
-    }
-
-    /**
-     * Cancels a request.  Not currently exposed through API.
-     */
+    /** Cancels a request.  Not currently exposed through API. */
     abstract void cancel();
 
     /**
      * Cancels a request with a cause.  Not currently exposed through API.
      */
     abstract void cancel(IOException cause);
+
+    /**
+     * Called when the exchange is released, so that cleanup actions may be
+     * performed - such as deregistering callbacks.
+     * Typically released is called during upgrade, when an HTTP/2 stream
+     * takes over from an Http1Exchange, or when a new exchange is created
+     * during a multi exchange before the final response body was received.
+     */
+    abstract void released();
+
+    /**
+     * Called when the exchange is completed, so that cleanup actions may be
+     * performed - such as deregistering callbacks.
+     * Typically, completed is called at the end of the exchange, when the
+     * final response body has been received (or an error has caused the
+     * completion of the exchange).
+     */
+    abstract void completed();
+
+    /**
+     * Returns true if this exchange was canceled.
+     * @return true if this exchange was canceled.
+     */
+    abstract boolean isCanceled();
+
+    /**
+     * Returns the cause for which this exchange was canceled, if available.
+     * @return the cause for which this exchange was canceled, if available.
+     */
+    abstract Throwable getCancelCause();
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExecutorWrapper.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, 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.incubator.http;
-
-import java.net.SocketPermission;
-import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.util.concurrent.Executor;
-import jdk.internal.misc.InnocuousThread;
-
-/**
- * Wraps the supplied user Executor
- *
- * when a Security manager set, the correct access control context is used to execute task
- *
- * The access control context is captured at creation time of this object
- */
-class ExecutorWrapper {
-
-    final Executor userExecutor; // the undeerlying executor provided by user
-    final Executor executor; // the executur which wraps the user's one
-    final AccessControlContext acc;
-    final ClassLoader ccl;
-
-    public ExecutorWrapper(Executor userExecutor, AccessControlContext acc) {
-        this.userExecutor = userExecutor;
-        this.acc = acc;
-        this.ccl = getCCL();
-        if (System.getSecurityManager() == null) {
-            this.executor = userExecutor;
-        } else {
-            this.executor = this::run;
-        }
-    }
-
-    private ClassLoader getCCL() {
-        return AccessController.doPrivileged(
-            (PrivilegedAction<ClassLoader>) () -> {
-                return Thread.currentThread().getContextClassLoader();
-            }
-        );
-    }
-
-    /**
-     * This is only used for the default HttpClient to deal with
-     * different application contexts that might be using it.
-     * The default client uses InnocuousThreads in its Executor.
-     */
-    private void prepareThread() {
-        final Thread me = Thread.currentThread();
-        if (!(me instanceof InnocuousThread))
-            return;
-        InnocuousThread innocuousMe = (InnocuousThread)me;
-
-        AccessController.doPrivileged(
-            (PrivilegedAction<Void>) () -> {
-                innocuousMe.setContextClassLoader(ccl);
-                innocuousMe.eraseThreadLocals();
-                return null;
-            }
-        );
-    }
-
-
-    void run(Runnable r) {
-        prepareThread();
-        try {
-            userExecutor.execute(r); // all throwables must be caught
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
-    public Executor userExecutor() {
-        return userExecutor;
-    }
-
-    public Executor executor() {
-        return executor;
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1AsyncReceiver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,643 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
+import jdk.incubator.http.internal.common.Utils;
+
+
+/**
+ * A helper class that will queue up incoming data until the receiving
+ * side is ready to handle it.
+ */
+class Http1AsyncReceiver {
+
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+    /**
+     * A delegate that can asynchronously receive data from an upstream flow,
+     * parse, it, then possibly transform it and either store it (response
+     * headers) or possibly pass it to a downstream subscriber (response body).
+     * Usually, there will be one Http1AsyncDelegate in charge of receiving
+     * and parsing headers, and another one in charge of receiving, parsing,
+     * and forwarding body. Each will sequentially subscribe with the
+     * Http1AsyncReceiver in turn. There may be additional delegates which
+     * subscribe to the Http1AsyncReceiver, mainly for the purpose of handling
+     * errors while the connection is busy transmitting the request body and the
+     * Http1Exchange::readBody method hasn't been called yet, and response
+     * delegates haven't subscribed yet.
+     */
+    static interface Http1AsyncDelegate {
+        /**
+         * Receives and handles a byte buffer reference.
+         * @param ref A byte buffer reference coming from upstream.
+         * @return false, if the byte buffer reference should be kept in the queue.
+         *         Usually, this means that either the byte buffer reference
+         *         was handled and parsing is finished, or that the receiver
+         *         didn't handle the byte reference at all.
+         *         There may or may not be any remaining data in the
+         *         byte buffer, and the byte buffer reference must not have
+         *         been cleared.
+         *         true, if the byte buffer reference was fully read and
+         *         more data can be received.
+         */
+        public boolean tryAsyncReceive(ByteBuffer ref);
+
+        /**
+         * Called when an exception is raised.
+         * @param ex The raised Throwable.
+         */
+        public void onReadError(Throwable ex);
+
+        /**
+         * Must be called before any other method on the delegate.
+         * The subscription can be either used directly by the delegate
+         * to request more data (e.g. if the delegate is a header parser),
+         * or can be forwarded to a downstream subscriber (if the delegate
+         * is a body parser that wraps a response BodyProcessor).
+         * In all cases, it is the responsibility of the delegate to ensure
+         * that request(n) and demand.tryDecrement() are called appropriately.
+         * No data will be sent to {@code tryAsyncReceive} unless
+         * the subscription has some demand.
+         *
+         * @param s A subscription that allows the delegate to control the
+         *          data flow.
+         */
+        public void onSubscribe(AbstractSubscription s);
+
+        /**
+         * Returns the subscription that was passed to {@code onSubscribe}
+         * @return the subscription that was passed to {@code onSubscribe}..
+         */
+        public AbstractSubscription subscription();
+
+    }
+
+    /**
+     * A simple subclass of AbstractSubscription that ensures the
+     * SequentialScheduler will be run when request() is called and demand
+     * becomes positive again.
+     */
+    private static final class Http1AsyncDelegateSubscription
+            extends AbstractSubscription
+    {
+        private final Runnable onCancel;
+        private final SequentialScheduler scheduler;
+        Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
+                                       Runnable onCancel) {
+            this.scheduler = scheduler;
+            this.onCancel = onCancel;
+        }
+        @Override
+        public void request(long n) {
+            final Demand demand = demand();
+            if (demand.increase(n)) {
+                scheduler.runOrSchedule();
+            }
+        }
+        @Override
+        public void cancel() { onCancel.run();}
+    }
+
+    private final ConcurrentLinkedDeque<ByteBuffer> queue
+            = new ConcurrentLinkedDeque<>();
+    private final SequentialScheduler scheduler =
+            new SequentialScheduler(new SynchronizedRestartableTask(this::flush));
+    private final Executor executor;
+    private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
+    private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
+    private final AtomicLong received = new AtomicLong();
+    final AtomicBoolean canRequestMore = new AtomicBoolean();
+
+    private volatile Throwable error;
+    private volatile Http1AsyncDelegate delegate;
+    // This reference is only used to prevent early GC of the exchange.
+    private volatile Http1Exchange<?>  owner;
+    // Only used for checking whether we run on the selector manager thread.
+    private final HttpClientImpl client;
+    private boolean retry;
+
+    public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
+        this.pendingDelegateRef = new AtomicReference<>();
+        this.executor = executor;
+        this.owner = owner;
+        this.client = owner.client;
+    }
+
+    // This is the main loop called by the SequentialScheduler.
+    // It attempts to empty the queue until the scheduler is stopped,
+    // or the delegate is unregistered, or the delegate is unable to
+    // process the data (because it's not ready or already done), which
+    // it signals by returning 'true';
+    private void flush() {
+        ByteBuffer buf;
+        try {
+            assert !client.isSelectorThread() :
+                    "Http1AsyncReceiver::flush should not run in the selector: "
+                    + Thread.currentThread().getName();
+
+            // First check whether we have a pending delegate that has
+            // just subscribed, and if so, create a Subscription for it
+            // and call onSubscribe.
+            handlePendingDelegate();
+
+            // Then start emptying the queue, if possible.
+            while ((buf = queue.peek()) != null) {
+                Http1AsyncDelegate delegate = this.delegate;
+                debug.log(Level.DEBUG, "Got %s bytes for delegate %s",
+                                       buf.remaining(), delegate);
+                if (!hasDemand(delegate)) {
+                    // The scheduler will be invoked again later when the demand
+                    // becomes positive.
+                    return;
+                }
+
+                assert delegate != null;
+                debug.log(Level.DEBUG, "Forwarding %s bytes to delegate %s",
+                          buf.remaining(), delegate);
+                // The delegate has demand: feed it the next buffer.
+                if (!delegate.tryAsyncReceive(buf)) {
+                    final long remaining = buf.remaining();
+                    debug.log(Level.DEBUG, () -> {
+                        // If the scheduler is stopped, the queue may already
+                        // be empty and the reference may already be released.
+                        String remstr = scheduler.isStopped() ? "" :
+                                " remaining in ref: "
+                                + remaining;
+                        remstr =  remstr
+                                + " total remaining: " + remaining();
+                        return "Delegate done: " + remaining;
+                    });
+                    canRequestMore.set(false);
+                    // The last buffer parsed may have remaining unparsed bytes.
+                    // Don't take it out of the queue.
+                    return; // done.
+                }
+
+                // removed parsed buffer from queue, and continue with next
+                // if available
+                ByteBuffer parsed = queue.remove();
+                canRequestMore.set(queue.isEmpty());
+                assert parsed == buf;
+            }
+
+            // queue is empty: let's see if we should request more
+            checkRequestMore();
+
+        } catch (Throwable t) {
+            Throwable x = error;
+            if (x == null) error = t; // will be handled in the finally block
+            debug.log(Level.DEBUG, "Unexpected error caught in flush()", t);
+        } finally {
+            // Handles any pending error.
+            // The most recently subscribed delegate will get the error.
+            checkForErrors();
+        }
+    }
+
+    /**
+     * Must be called from within the scheduler main loop.
+     * Handles any pending errors by calling delegate.onReadError().
+     * If the error can be forwarded to the delegate, stops the scheduler.
+     */
+    private void checkForErrors() {
+        // Handles any pending error.
+        // The most recently subscribed delegate will get the error.
+        // If the delegate is null, the error will be handled by the next
+        // delegate that subscribes.
+        // If the queue is not empty, wait until it it is empty before
+        // handling the error.
+        Http1AsyncDelegate delegate = pendingDelegateRef.get();
+        if (delegate == null) delegate = this.delegate;
+        Throwable x = error;
+        if (delegate != null && x != null && queue.isEmpty()) {
+            // forward error only after emptying the queue.
+            final Object captured = delegate;
+            debug.log(Level.DEBUG, () -> "flushing " + x
+                    + "\n\t delegate: " + captured
+                    + "\t\t queue.isEmpty: " + queue.isEmpty());
+            scheduler.stop();
+            delegate.onReadError(x);
+        }
+    }
+
+    /**
+     * Must be called from within the scheduler main loop.
+     * Figure out whether more data should be requested from the
+     * Http1TubeSubscriber.
+     */
+    private void checkRequestMore() {
+        Http1AsyncDelegate delegate = this.delegate;
+        boolean more = this.canRequestMore.get();
+        boolean hasDemand = hasDemand(delegate);
+        debug.log(Level.DEBUG, () -> "checkRequestMore: "
+                  + "canRequestMore=" + more + ", hasDemand=" + hasDemand
+                  + (delegate == null ? ", delegate=null" : ""));
+        if (hasDemand) {
+            subscriber.requestMore();
+        }
+    }
+
+    /**
+     * Must be called from within the scheduler main loop.
+     * Return true if the delegate is not null and has some demand.
+     * @param delegate The Http1AsyncDelegate delegate
+     * @return true if the delegate is not null and has some demand
+     */
+    private boolean hasDemand(Http1AsyncDelegate delegate) {
+        if (delegate == null) return false;
+        AbstractSubscription subscription = delegate.subscription();
+        long demand = subscription.demand().get();
+        debug.log(Level.DEBUG, "downstream subscription demand is %s", demand);
+        return demand > 0;
+    }
+
+    /**
+     * Must be called from within the scheduler main loop.
+     * Handles pending delegate subscription.
+     * Return true if there was some pending delegate subscription and a new
+     * delegate was subscribed, false otherwise.
+     *
+     * @return true if there was some pending delegate subscription and a new
+     *         delegate was subscribed, false otherwise.
+     */
+    private boolean handlePendingDelegate() {
+        Http1AsyncDelegate pending = pendingDelegateRef.get();
+        if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
+            Http1AsyncDelegate delegate = this.delegate;
+            if (delegate != null) unsubscribe(delegate);
+            Runnable cancel = () -> {
+                debug.log(Level.DEBUG, "Downstream subscription cancelled by %s", pending);
+                unsubscribe(pending);
+            };
+            // The subscription created by a delegate is only loosely
+            // coupled with the upstream subscription. This is partly because
+            // the header/body parser work with a flow of ByteBuffer, whereas
+            // we have a flow List<ByteBuffer> upstream.
+            Http1AsyncDelegateSubscription subscription =
+                    new Http1AsyncDelegateSubscription(scheduler, cancel);
+            pending.onSubscribe(subscription);
+            this.delegate = delegate = pending;
+            final Object captured = delegate;
+            debug.log(Level.DEBUG, () -> "delegate is now " + captured
+                  + ", demand=" + subscription.demand().get()
+                  + ", canRequestMore=" + canRequestMore.get()
+                  + ", queue.isEmpty=" + queue.isEmpty());
+            return true;
+        }
+        return false;
+    }
+
+    synchronized void setRetryOnError(boolean retry) {
+        this.retry = retry;
+    }
+
+    void clear() {
+        debug.log(Level.DEBUG, "cleared");
+        this.pendingDelegateRef.set(null);
+        this.delegate = null;
+        this.owner = null;
+    }
+
+    void subscribe(Http1AsyncDelegate delegate) {
+        synchronized(this) {
+            pendingDelegateRef.set(delegate);
+        }
+        if (queue.isEmpty()) {
+            canRequestMore.set(true);
+        }
+        debug.log(Level.DEBUG, () ->
+                "Subscribed pending " + delegate + " queue.isEmpty: "
+                + queue.isEmpty());
+        // Everything may have been received already. Make sure
+        // we parse it.
+        if (client.isSelectorThread()) {
+            scheduler.deferOrSchedule(executor);
+        } else {
+            scheduler.runOrSchedule();
+        }
+    }
+
+    // Used for debugging only!
+    long remaining() {
+        return Utils.remaining(queue.toArray(new ByteBuffer[0]));
+    }
+
+    void unsubscribe(Http1AsyncDelegate delegate) {
+        synchronized(this) {
+            if (this.delegate == delegate) {
+                debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
+                this.delegate = null;
+            }
+        }
+    }
+
+    // Callback: Consumer of ByteBufferReference
+    private void asyncReceive(ByteBuffer buf) {
+        debug.log(Level.DEBUG, "Putting %s bytes into the queue", buf.remaining());
+        received.addAndGet(buf.remaining());
+        queue.offer(buf);
+
+        // This callback is called from within the selector thread.
+        // Use an executor here to avoid doing the heavy lifting in the
+        // selector.
+        scheduler.deferOrSchedule(executor);
+    }
+
+    // Callback: Consumer of Throwable
+    void onReadError(Throwable ex) {
+        Http1AsyncDelegate delegate;
+        Throwable recorded;
+        debug.log(Level.DEBUG, "onError: %s", (Object) ex);
+        synchronized (this) {
+            delegate = this.delegate;
+            recorded = error;
+            if (recorded == null) {
+                // retry is set to true by HttpExchange when the connection is
+                // already connected, which means it's been retrieved from
+                // the pool.
+                if (retry && (ex instanceof IOException)) {
+                    // could be either EOFException, or
+                    // IOException("connection reset by peer), or
+                    // SSLHandshakeException resulting from the server having
+                    // closed the SSL session.
+                    if (received.get() == 0) {
+                        // If we receive such an exception before having
+                        // received any byte, then in this case, we will
+                        // throw ConnectionExpiredException
+                        // to try & force a retry of the request.
+                        retry = false;
+                        ex = new ConnectionExpiredException(
+                                "subscription is finished", ex);
+                    }
+                }
+                error = ex;
+            }
+            final Throwable t = (recorded == null ? ex : recorded);
+            debug.log(Level.DEBUG, () -> "recorded " + t
+                    + "\n\t delegate: " + delegate
+                    + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
+        }
+        if (queue.isEmpty() || pendingDelegateRef.get() != null) {
+            // This callback is called from within the selector thread.
+            // Use an executor here to avoid doing the heavy lifting in the
+            // selector.
+            scheduler.deferOrSchedule(executor);
+        }
+    }
+
+    void stop() {
+        debug.log(Level.DEBUG, "stopping");
+        scheduler.stop();
+        delegate = null;
+        owner  = null;
+    }
+
+    /**
+     * Returns the TubeSubscriber for reading from the connection flow.
+     * @return the TubeSubscriber for reading from the connection flow.
+     */
+    TubeSubscriber subscriber() {
+        return subscriber;
+    }
+
+    /**
+     * A simple tube subscriber for reading from the connection flow.
+     */
+    final class Http1TubeSubscriber implements TubeSubscriber {
+        volatile Flow.Subscription subscription;
+        volatile boolean completed;
+        volatile boolean dropped;
+
+        public void onSubscribe(Flow.Subscription subscription) {
+            // supports being called multiple time.
+            // doesn't cancel the previous subscription, since that is
+            // most probably the same as the new subscription.
+            assert this.subscription == null || dropped == false;
+            this.subscription = subscription;
+            dropped = false;
+            canRequestMore.set(true);
+            if (delegate != null) {
+                scheduler.deferOrSchedule(executor);
+            }
+        }
+
+        void requestMore() {
+            Flow.Subscription s = subscription;
+            if (s == null) return;
+            if (canRequestMore.compareAndSet(true, false)) {
+                if (!completed && !dropped) {
+                    debug.log(Level.DEBUG,
+                        "Http1TubeSubscriber: requesting one more from upstream");
+                    s.request(1);
+                    return;
+                }
+            }
+            debug.log(Level.DEBUG, "Http1TubeSubscriber: no need to request more");
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            canRequestMore.set(item.isEmpty());
+            for (ByteBuffer buffer : item) {
+                asyncReceive(buffer);
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            onReadError(throwable);
+            completed = true;
+        }
+
+        @Override
+        public void onComplete() {
+            onReadError(new EOFException("EOF reached while reading"));
+            completed = true;
+        }
+
+        public void dropSubscription() {
+            debug.log(Level.DEBUG, "Http1TubeSubscriber: dropSubscription");
+            // we could probably set subscription to null here...
+            // then we might not need the 'dropped' boolean?
+            dropped = true;
+        }
+
+    }
+
+    // Drains the content of the queue into a single ByteBuffer.
+    // The scheduler must be permanently stopped before calling drain().
+    ByteBuffer drain(ByteBuffer initial) {
+        // Revisit: need to clean that up.
+        //
+        ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
+        assert scheduler.isStopped();
+
+        if (queue.isEmpty()) return b;
+
+        // sanity check: we shouldn't have queued the same
+        // buffer twice.
+        ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
+        assert java.util.stream.Stream.of(qbb)
+                .collect(Collectors.toSet())
+                .size() == qbb.length : debugQBB(qbb);
+
+        // compute the number of bytes in the queue, the number of bytes
+        // in the initial buffer
+        // TODO: will need revisiting - as it is not guaranteed that all
+        // data will fit in single BB!
+        int size = Utils.remaining(qbb, Integer.MAX_VALUE);
+        int remaining = b.remaining();
+        int free = b.capacity() - b.position() - remaining;
+        debug.log(Level.DEBUG,
+            "Flushing %s bytes from queue into initial buffer (remaining=%s, free=%s)",
+            size, remaining, free);
+
+        // check whether the initial buffer has enough space
+        if (size > free) {
+            debug.log(Level.DEBUG,
+                    "Allocating new buffer for initial: %s", (size + remaining));
+            // allocates a new buffer and copy initial to it
+            b = ByteBuffer.allocate(size + remaining);
+            Utils.copy(initial, b);
+            assert b.position() == remaining;
+            b.flip();
+            assert b.position() == 0;
+            assert b.limit() == remaining;
+            assert b.remaining() == remaining;
+        }
+
+        // store position and limit
+        int pos = b.position();
+        int limit = b.limit();
+        assert limit - pos == remaining;
+        assert b.capacity() >= remaining + size
+                : "capacity: " + b.capacity()
+                + ", remaining: " + b.remaining()
+                + ", size: " + size;
+
+        // prepare to copy the content of the queue
+        b.position(limit);
+        b.limit(pos + remaining + size);
+        assert b.remaining() >= size :
+                "remaining: " + b.remaining() + ", size: " + size;
+
+        // copy the content of the queue
+        int count = 0;
+        for (int i=0; i<qbb.length; i++) {
+            ByteBuffer b2 = qbb[i];
+            int r = b2.remaining();
+            assert b.remaining() >= r : "need at least " + r + " only "
+                    + b.remaining() + " available";
+            int copied = Utils.copy(b2, b);
+            assert copied == r : "copied="+copied+" available="+r;
+            assert b2.remaining() == 0;
+            count += copied;
+        }
+        assert count == size;
+        assert b.position() == pos + remaining + size :
+                "b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
+
+        // reset limit and position
+        b.limit(limit+size);
+        b.position(pos);
+
+        // we can clear the refs
+        queue.clear();
+        final ByteBuffer bb = b;
+        debug.log(Level.DEBUG, () -> "Initial buffer now has " + bb.remaining()
+                + " pos=" + bb.position() + " limit=" + bb.limit());
+
+        return b;
+    }
+
+    private String debugQBB(ByteBuffer[] qbb) {
+        StringBuilder msg = new StringBuilder();
+        List<ByteBuffer> lbb = Arrays.asList(qbb);
+        Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
+
+        int uniquebb = sbb.size();
+        msg.append("qbb: ").append(lbb.size())
+           .append(" (unique: ").append(uniquebb).append("), ")
+           .append("duplicates: ");
+        String sep = "";
+        for (ByteBuffer b : lbb) {
+            if (!sbb.remove(b)) {
+                msg.append(sep)
+                   .append(String.valueOf(b))
+                   .append("[remaining=")
+                   .append(b.remaining())
+                   .append(", position=")
+                   .append(b.position())
+                   .append(", capacity=")
+                   .append(b.capacity())
+                   .append("]");
+                sep = ", ";
+            }
+        }
+        return msg.toString();
+    }
+
+    volatile String dbgTag;
+    String dbgString() {
+        String tag = dbgTag;
+        if (tag == null) {
+            String flowTag = null;
+            Http1Exchange<?> exchg = owner;
+            Object flow = (exchg != null)
+                    ? exchg.connection().getConnectionFlow()
+                    : null;
+            flowTag = tag = flow == null ? null: (String.valueOf(flow));
+            if (flowTag != null) {
+                dbgTag = tag = flowTag + " Http1AsyncReceiver";
+            } else {
+                tag = "Http1AsyncReceiver";
+            }
+        }
+        return tag;
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,40 +26,127 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.http.HttpResponse.BodyProcessor;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
-import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
 import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
 import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
 
 /**
- * Encapsulates one HTTP/1.1 request/responseAsync exchange.
+ * Encapsulates one HTTP/1.1 request/response exchange.
  */
 class Http1Exchange<T> extends ExchangeImpl<T> {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+    private static final System.Logger DEBUG_LOGGER =
+            Utils.getDebugLogger("Http1Exchange"::toString, DEBUG);
+
     final HttpRequestImpl request;        // main request
-    private final List<CompletableFuture<?>> operations; // used for cancel
     final Http1Request requestAction;
     private volatile Http1Response<T> response;
-    // use to record possible cancellation raised before any operation
-    // has been initiated.
-    private IOException failed;
     final HttpConnection connection;
     final HttpClientImpl client;
     final Executor executor;
-    volatile ByteBuffer buffer; // used for receiving
+    private volatile ByteBuffer buffer; // used for receiving
+    private final Http1AsyncReceiver asyncReceiver;
+
+    /** Records a possible cancellation raised before any operation
+     * has been initiated, or an error received while sending the request. */
+    private Throwable failed;
+    private final List<CompletableFuture<?>> operations; // used for cancel
+
+    /** Must be held when operating on any internal state or data. */
+    private final Object lock = new Object();
+
+    /** Holds the outgoing data, either the headers or a request body part. Or
+     * an error from the request body publisher. At most there can be ~2 pieces
+     * of outgoing data ( onComplete|onError can be invoked without demand ).*/
+    final ConcurrentLinkedDeque<DataPair> outgoing = new ConcurrentLinkedDeque<>();
+
+    /** The write publisher, responsible for writing the complete request ( both
+     * headers and body ( if any ). */
+    private final Http1Publisher writePublisher = new Http1Publisher();
+
+    /** Completed when the header have been published, or there is an error */
+    private volatile CompletableFuture<ExchangeImpl<T>> headersSentCF  = new CompletableFuture<>();
+     /** Completed when the body has been published, or there is an error */
+    private volatile CompletableFuture<ExchangeImpl<T>> bodySentCF = new CompletableFuture<>();
+
+    /** The subscriber to the request's body published. Maybe null. */
+    private volatile Http1BodySubscriber bodySubscriber;
+
+    enum State { INITIAL,
+                 HEADERS,
+                 BODY,
+                 ERROR,          // terminal state
+                 COMPLETING,
+                 COMPLETED }     // terminal state
+
+    private State state = State.INITIAL;
+
+    /** A carrier for either data or an error. Used to carry data, and communicate
+     * errors from the request ( both headers and body ) to the exchange. */
+    static class DataPair {
+        Throwable throwable;
+        List<ByteBuffer> data;
+        DataPair(List<ByteBuffer> data, Throwable throwable){
+            this.data = data;
+            this.throwable = throwable;
+        }
+        @Override
+        public String toString() {
+            return "DataPair [data=" + data + ", throwable=" + throwable + "]";
+        }
+    }
+
+    /** An abstract supertype for HTTP/1.1 body subscribers. There are two
+     * concrete implementations: {@link Http1Request.StreamSubscriber}, and
+     * {@link Http1Request.FixedContentSubscriber}, for receiving chunked and
+     * fixed length bodies, respectively. */
+    static abstract class Http1BodySubscriber implements Flow.Subscriber<ByteBuffer> {
+        protected volatile Flow.Subscription subscription;
+        protected volatile boolean complete;
+
+        /** Final sentinel in the stream of request body. */
+        static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
+
+        void request(long n) {
+            DEBUG_LOGGER.log(Level.DEBUG, () ->
+                "Http1BodySubscriber requesting " + n + ", from " + subscription);
+            subscription.request(n);
+        }
+
+        static Http1BodySubscriber completeSubscriber() {
+            return new Http1BodySubscriber() {
+                @Override public void onSubscribe(Flow.Subscription subscription) { error(); }
+                @Override public void onNext(ByteBuffer item) { error(); }
+                @Override public void onError(Throwable throwable) { error(); }
+                @Override public void onComplete() { error(); }
+                private void error() {
+                    throw new InternalError("should not reach here");
+                }
+            };
+        }
+    }
 
     @Override
     public String toString() {
-        return request.toString();
+        return "HTTP/1.1 " + request.toString();
     }
 
     HttpRequestImpl request() {
@@ -74,44 +161,157 @@
         this.client = exchange.client();
         this.executor = exchange.executor();
         this.operations = new LinkedList<>();
-        this.buffer = Utils.EMPTY_BYTEBUFFER;
+        operations.add(headersSentCF);
+        operations.add(bodySentCF);
+        this.buffer = Utils.EMPTY_BYTEBUFFER;  // TODO: need to check left over data?
         if (connection != null) {
             this.connection = connection;
         } else {
             InetSocketAddress addr = request.getAddress(client);
-            this.connection = HttpConnection.getConnection(addr, client, request);
+            this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
         }
-        this.requestAction = new Http1Request(request, client, this.connection);
+        this.requestAction = new Http1Request(request, client, this);
+        this.asyncReceiver = new Http1AsyncReceiver(executor, this);
+        asyncReceiver.subscribe(new InitialErrorReceiver());
     }
 
+    /** An initial receiver that handles no data, but cancels the request if
+     * it receives an error. Will be replaced when reading response body. */
+    final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
+        volatile AbstractSubscription s;
+        @Override
+        public boolean tryAsyncReceive(ByteBuffer ref) {
+            return false;  // no data has been processed, leave it in the queue
+        }
 
+        @Override
+        public void onReadError(Throwable ex) {
+            cancelImpl(ex);
+        }
+
+        @Override
+        public void onSubscribe(AbstractSubscription s) {
+            this.s = s;
+        }
+
+        public AbstractSubscription subscription() {
+            return s;
+        }
+    }
+
+    @Override
     HttpConnection connection() {
         return connection;
     }
 
+    private void connectFlows(HttpConnection connection) {
+        FlowTube tube =  connection.getConnectionFlow();
+        debug.log(Level.DEBUG, "%s connecting flows", tube);
+
+        // Connect the flow to our Http1TubeSubscriber:
+        //   asyncReceiver.subscriber().
+        tube.connectFlows(writePublisher,
+                          asyncReceiver.subscriber());
+    }
 
     @Override
-    T readBody(BodyHandler<T> handler, boolean returnConnectionToPool)
-        throws IOException
-    {
-        BodyProcessor<T> processor = handler.apply(response.responseCode(),
-                                                   response.responseHeaders());
-        CompletableFuture<T> bodyCF = response.readBody(processor,
-                                                        returnConnectionToPool,
-                                                        this::executeInline);
-        try {
-            return bodyCF.join();
-        } catch (CompletionException e) {
-            throw Utils.getIOException(e);
+    CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+        // create the response before sending the request headers, so that
+        // the response can set the appropriate receivers.
+        debug.log(Level.DEBUG, "Sending headers only");
+        if (response == null) {
+            response = new Http1Response<>(connection, this, asyncReceiver);
         }
+
+        debug.log(Level.DEBUG, "response created in advance");
+        // If the first attempt to read something triggers EOF, or
+        // IOException("channel reset by peer"), we're going to retry.
+        // Instruct the asyncReceiver to throw ConnectionExpiredException
+        // to force a retry.
+        asyncReceiver.setRetryOnError(true);
+
+        CompletableFuture<Void> connectCF;
+        if (!connection.connected()) {
+            debug.log(Level.DEBUG, "initiating connect async");
+            connectCF = connection.connectAsync();
+            synchronized (lock) {
+                operations.add(connectCF);
+            }
+        } else {
+            connectCF = new CompletableFuture<>();
+            connectCF.complete(null);
+        }
+
+        return connectCF
+                .thenCompose(unused -> {
+                    CompletableFuture<Void> cf = new CompletableFuture<>();
+                    try {
+                        connectFlows(connection);
+
+                        debug.log(Level.DEBUG, "requestAction.headers");
+                        List<ByteBuffer> data = requestAction.headers();
+                        synchronized (lock) {
+                            state = State.HEADERS;
+                        }
+                        debug.log(Level.DEBUG, "setting outgoing with headers");
+                        assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
+                        appendToOutgoing(data);
+                        cf.complete(null);
+                        return cf;
+                    } catch (Throwable t) {
+                        debug.log(Level.DEBUG, "Failed to send headers: %s", t);
+                        connection.close();
+                        cf.completeExceptionally(t);
+                        return cf;
+                    } })
+                .thenCompose(unused -> headersSentCF);
     }
 
-    private void executeInline(Runnable r) {
-        r.run();
+    @Override
+    CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+        assert headersSentCF.isDone();
+        try {
+            bodySubscriber = requestAction.continueRequest();
+            if (bodySubscriber == null) {
+                bodySubscriber = Http1BodySubscriber.completeSubscriber();
+                appendToOutgoing(Http1BodySubscriber.COMPLETED);
+            } else {
+                bodySubscriber.request(1);  // start
+            }
+        } catch (Throwable t) {
+            connection.close();
+            bodySentCF.completeExceptionally(t);
+        }
+        return bodySentCF;
     }
 
-    synchronized ByteBuffer getBuffer() {
-        return buffer;
+    @Override
+    CompletableFuture<Response> getResponseAsync(Executor executor) {
+        CompletableFuture<Response> cf = response.readHeadersAsync(executor);
+        Throwable cause;
+        synchronized (lock) {
+            operations.add(cf);
+            cause = failed;
+            failed = null;
+        }
+
+        if (cause != null) {
+            Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
+                            + "\n\tCompleting exceptionally with {2}\n",
+                         request.uri(),
+                         request.timeout().isPresent() ?
+                            // calling duration.toMillis() can throw an exception.
+                            // this is just debugging, we don't care if it overflows.
+                            (request.timeout().get().getSeconds() * 1000
+                             + request.timeout().get().getNano() / 1000000) : -1,
+                         cause);
+            boolean acknowledged = cf.completeExceptionally(cause);
+            debug.log(Level.DEBUG,
+                      () -> acknowledged
+                            ? ("completed response with " + cause)
+                            : ("response already completed, ignoring " + cause));
+        }
+        return cf;
     }
 
     @Override
@@ -119,53 +319,31 @@
                                        boolean returnConnectionToPool,
                                        Executor executor)
     {
-        BodyProcessor<T> processor = handler.apply(response.responseCode(),
-                                                   response.responseHeaders());
-        CompletableFuture<T> bodyCF = response.readBody(processor,
+        BodySubscriber<T> bs = handler.apply(response.responseCode(),
+                                             response.responseHeaders());
+        CompletableFuture<T> bodyCF = response.readBody(bs,
                                                         returnConnectionToPool,
                                                         executor);
         return bodyCF;
     }
 
-    @Override
-    void sendHeadersOnly() throws IOException, InterruptedException {
-        try {
-            if (!connection.connected()) {
-                connection.connect();
-            }
-            requestAction.sendHeadersOnly();
-        } catch (Throwable e) {
-            connection.close();
-            throw e;
+    ByteBuffer getBuffer() {
+        synchronized (lock) {
+            asyncReceiver.stop();
+            this.buffer = asyncReceiver.drain(this.buffer);
+            return this.buffer;
         }
     }
 
-    @Override
-    void sendBody() throws IOException {
-        try {
-            requestAction.continueRequest();
-        } catch (Throwable e) {
-            connection.close();
-            throw e;
-        }
+    void released() {
+        Http1Response<T> resp = this.response;
+        if (resp != null) resp.completed();
+        asyncReceiver.clear();
     }
 
-    @Override
-    Response getResponse() throws IOException {
-        try {
-            response = new Http1Response<>(connection, this);
-            response.readHeaders();
-            Response r = response.response();
-            buffer = response.getBuffer();
-            return r;
-        } catch (Throwable t) {
-            connection.close();
-            throw t;
-        }
-    }
-
-    private void closeConnection() {
-        connection.close();
+    void completed() {
+        Http1Response<T> resp = this.response;
+        if (resp != null) resp.completed();
     }
 
     /**
@@ -174,7 +352,7 @@
      */
     @Override
     void cancel() {
-        cancel(new IOException("Request cancelled"));
+        cancelImpl(new IOException("Request cancelled"));
     }
 
     /**
@@ -182,66 +360,255 @@
      * If not it closes the connection and completes all pending operations
      */
     @Override
-    synchronized void cancel(IOException cause) {
-        if (requestAction != null && requestAction.finished()
-                && response != null && response.finished()) {
-            return;
-        }
-        connection.close();
+    void cancel(IOException cause) {
+        cancelImpl(cause);
+    }
+
+    private void cancelImpl(Throwable cause) {
+        LinkedList<CompletableFuture<?>> toComplete = null;
         int count = 0;
-        if (operations.isEmpty()) {
-            failed = cause;
-            Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
-                         + "\n\tCan''t cancel yet with {2}",
-                         request.uri(),
-                         request.duration() == null ? -1 :
-                         // calling duration.toMillis() can throw an exception.
-                         // this is just debugging, we don't care if it overflows.
-                         (request.duration().getSeconds() * 1000
-                          + request.duration().getNano() / 1000000),
-                         cause);
-        } else {
-            for (CompletableFuture<?> cf : operations) {
-                cf.completeExceptionally(cause);
-                count++;
+        synchronized (lock) {
+            if (failed == null)
+                failed = cause;
+            if (requestAction != null && requestAction.finished()
+                    && response != null && response.finished()) {
+                return;
+            }
+            connection.close();   // TODO: ensure non-blocking if holding the lock
+            writePublisher.writeScheduler.stop();
+            if (operations.isEmpty()) {
+                Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
+                                + "\n\tCan''t cancel yet with {2}",
+                             request.uri(),
+                             request.timeout().isPresent() ?
+                                // calling duration.toMillis() can throw an exception.
+                                // this is just debugging, we don't care if it overflows.
+                                (request.timeout().get().getSeconds() * 1000
+                                 + request.timeout().get().getNano() / 1000000) : -1,
+                             cause);
+            } else {
+                for (CompletableFuture<?> cf : operations) {
+                    if (!cf.isDone()) {
+                        if (toComplete == null) toComplete = new LinkedList<>();
+                        toComplete.add(cf);
+                        count++;
+                    }
+                }
+                operations.clear();
             }
         }
         Log.logError("Http1Exchange.cancel: count=" + count);
+        if (toComplete != null) {
+            // We might be in the selector thread in case of timeout, when
+            // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
+            // There may or may not be other places that reach here
+            // from the SelectorManager thread, so just make sure we
+            // don't complete any CF from within the selector manager
+            // thread.
+            Executor exec = client.isSelectorThread()
+                            ? executor
+                            : this::runInline;
+            while (!toComplete.isEmpty()) {
+                CompletableFuture<?> cf = toComplete.poll();
+                exec.execute(() -> {
+                    if (cf.completeExceptionally(cause)) {
+                        debug.log(Level.DEBUG, "completed cf with %s",
+                                 (Object) cause);
+                    }
+                });
+            }
+        }
+    }
+
+    private void runInline(Runnable run) {
+        assert !client.isSelectorThread();
+        run.run();
     }
 
-    CompletableFuture<Response> getResponseAsyncImpl(Executor executor) {
-        return MinimalFuture.supply( () -> {
-            response = new Http1Response<>(connection, Http1Exchange.this);
-            response.readHeaders();
-            Response r = response.response();
-            buffer = response.getBuffer();
-            return r;
-        }, executor);
+    /** Returns true if this exchange was canceled. */
+    boolean isCanceled() {
+        synchronized (lock) {
+            return failed != null;
+        }
+    }
+
+    /** Returns the cause for which this exchange was canceled, if available. */
+    Throwable getCancelCause() {
+        synchronized (lock) {
+            return failed;
+        }
+    }
+
+    /** Convenience for {@link #appendToOutgoing(DataPair)}, with just a Throwable. */
+    void appendToOutgoing(Throwable throwable) {
+        appendToOutgoing(new DataPair(null, throwable));
+    }
+
+    /** Convenience for {@link #appendToOutgoing(DataPair)}, with just data. */
+    void appendToOutgoing(List<ByteBuffer> item) {
+        appendToOutgoing(new DataPair(item, null));
+    }
+
+    private void appendToOutgoing(DataPair dp) {
+        debug.log(Level.DEBUG, "appending to outgoing " + dp);
+        outgoing.add(dp);
+        writePublisher.writeScheduler.runOrSchedule();
+    }
+
+    /** Tells whether, or not, there is any outgoing data that can be published,
+     * or if there is an error. */
+    private boolean hasOutgoing() {
+        return !outgoing.isEmpty();
     }
 
-    @Override
-    CompletableFuture<Response> getResponseAsync(Executor executor) {
-        CompletableFuture<Response> cf =
-            connection.whenReceivingResponse()
-                      .thenCompose((v) -> getResponseAsyncImpl(executor));
-        IOException cause;
-        synchronized(this) {
-            operations.add(cf);
-            cause = failed;
-            failed = null;
+    // Invoked only by the publisher
+    // ALL tasks should execute off the Selector-Manager thread
+    /** Returns the next portion of the HTTP request, or the error. */
+    private DataPair getOutgoing() {
+        final Executor exec = client.theExecutor();
+        final DataPair dp = outgoing.pollFirst();
+
+        if (dp == null)  // publisher has not published anything yet
+            return null;
+
+        synchronized (lock) {
+            if (dp.throwable != null) {
+                state = State.ERROR;
+                exec.execute(() -> {
+                    connection.close();
+                    headersSentCF.completeExceptionally(dp.throwable);
+                    bodySentCF.completeExceptionally(dp.throwable);
+                });
+                return dp;
+            }
+
+            switch (state) {
+                case HEADERS:
+                    state = State.BODY;
+                    // completeAsync, since dependent tasks should run in another thread
+                    debug.log(Level.DEBUG, "initiating completion of headersSentCF");
+                    headersSentCF.completeAsync(() -> this, exec);
+                    break;
+                case BODY:
+                    if (dp.data == Http1BodySubscriber.COMPLETED) {
+                        state = State.COMPLETING;
+                        debug.log(Level.DEBUG, "initiating completion of bodySentCF");
+                        bodySentCF.completeAsync(() -> this, exec);
+                    } else {
+                        debug.log(Level.DEBUG, "requesting more body from the subscriber");
+                        exec.execute(() -> bodySubscriber.request(1));
+                    }
+                    break;
+                case INITIAL:
+                case ERROR:
+                case COMPLETING:
+                case COMPLETED:
+                default:
+                    assert false : "Unexpected state:" + state;
+            }
+
+            return dp;
+        }
+    }
+
+    /** A Publisher of HTTP/1.1 headers and request body. */
+    final class Http1Publisher implements HttpConnection.HttpPublisher {
+
+        final System.Logger  debug = Utils.getDebugLogger(this::dbgString);
+        volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+        volatile boolean cancelled;
+        final Http1WriteSubscription subscription = new Http1WriteSubscription();
+        final Demand demand = new Demand();
+        final SequentialScheduler writeScheduler = new SequentialScheduler(
+                new SynchronizedRestartableTask(new WriteTask()));
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+            assert state == State.INITIAL;
+            Objects.requireNonNull(s);
+            assert subscriber == null;
+
+            subscriber = s;
+            debug.log(Level.DEBUG, "got subscriber: %s", s);
+            s.onSubscribe(subscription);
         }
-        if (cause != null) {
-            Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
-                         + "\n\tCompleting exceptionally with {2}\n",
-                         request.uri(),
-                         request.duration() == null ? -1 :
-                         // calling duration.toMillis() can throw an exception.
-                         // this is just debugging, we don't care if it overflows.
-                         (request.duration().getSeconds() * 1000
-                          + request.duration().getNano() / 1000000),
-                         cause);
-            cf.completeExceptionally(cause);
+
+        volatile String dbgTag;
+        String dbgString() {
+            String tag = dbgTag;
+            Object flow = connection.getConnectionFlow();
+            if (tag == null && flow != null) {
+                dbgTag = tag = "Http1Publisher(" + flow + ")";
+            } else if (tag == null) {
+                tag = "Http1Publisher(?)";
+            }
+            return tag;
         }
-        return cf;
+
+        final class WriteTask implements Runnable {
+            @Override
+            public void run() {
+                assert state != State.COMPLETED : "Unexpected state:" + state;
+                debug.log(Level.DEBUG, "WriteTask");
+                if (subscriber == null) {
+                    debug.log(Level.DEBUG, "no subscriber yet");
+                    return;
+                }
+                debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
+                if (hasOutgoing() && demand.tryDecrement()) {
+                    DataPair dp = getOutgoing();
+
+                    if (dp.throwable != null) {
+                        debug.log(Level.DEBUG, "onError");
+                        // Do not call the subscriber's onError, it is not required.
+                        writeScheduler.stop();
+                    } else {
+                        List<ByteBuffer> data = dp.data;
+                        if (data == Http1BodySubscriber.COMPLETED) {
+                            synchronized (lock) {
+                                assert state == State.COMPLETING : "Unexpected state:" + state;
+                                state = State.COMPLETED;
+                            }
+                            debug.log(Level.DEBUG,
+                                     "completed, stopping %s", writeScheduler);
+                            writeScheduler.stop();
+                            // Do nothing more. Just do not publish anything further.
+                            // The next Subscriber will eventually take over.
+
+                        } else {
+                            debug.log(Level.DEBUG, () ->
+                                    "onNext with " + Utils.remaining(data) + " bytes");
+                            subscriber.onNext(data);
+                        }
+                    }
+                }
+            }
+        }
+
+        final class Http1WriteSubscription implements Flow.Subscription {
+
+            @Override
+            public void request(long n) {
+                if (cancelled)
+                    return;  //no-op
+                demand.increase(n);
+                debug.log(Level.DEBUG,
+                        "subscription request(%d), demand=%s", n, demand);
+                writeScheduler.deferOrSchedule(client.theExecutor());
+            }
+
+            @Override
+            public void cancel() {
+                debug.log(Level.DEBUG, "subscription cancelled");
+                if (cancelled)
+                    return;  //no-op
+                cancelled = true;
+                writeScheduler.stop();
+            }
+        }
+    }
+
+    String dbgString() {
+        return "Http1Exchange";
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1HeaderParser.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+class Http1HeaderParser {
+
+    private static final char CR = '\r';
+    private static final char LF = '\n';
+    private static final char HT = '\t';
+    private static final char SP = ' ';
+
+    private StringBuilder sb = new StringBuilder();
+    private String statusLine;
+    private int responseCode;
+    private HttpHeaders headers;
+    private Map<String,List<String>> privateMap = new HashMap<>();
+
+    enum State { STATUS_LINE,
+                 STATUS_LINE_FOUND_CR,
+                 STATUS_LINE_END,
+                 STATUS_LINE_END_CR,
+                 HEADER,
+                 HEADER_FOUND_CR,
+                 HEADER_FOUND_LF,
+                 HEADER_FOUND_CR_LF,
+                 HEADER_FOUND_CR_LF_CR,
+                 FINISHED }
+
+    private State state = State.STATUS_LINE;
+
+    /** Returns the status-line. */
+    String statusLine() { return statusLine; }
+
+    /** Returns the response code. */
+    int responseCode() { return responseCode; }
+
+    /** Returns the headers, possibly empty. */
+    HttpHeaders headers() { assert state == State.FINISHED; return headers; }
+
+    /**
+     * Parses HTTP/1.X status-line and headers from the given bytes. Must be
+     * called successive times, with additional data, until returns true.
+     *
+     * All given ByteBuffers will be consumed, until ( possibly ) the last one
+     * ( when true is returned ), which may not be fully consumed.
+     *
+     * @param input the ( partial ) header data
+     * @return true iff the end of the headers block has been reached
+     */
+    boolean parse(ByteBuffer input) throws ProtocolException {
+        requireNonNull(input, "null input");
+
+        while (input.hasRemaining() && state != State.FINISHED) {
+            switch (state) {
+                case STATUS_LINE:
+                    readResumeStatusLine(input);
+                    break;
+                case STATUS_LINE_FOUND_CR:
+                    readStatusLineFeed(input);
+                    break;
+                case STATUS_LINE_END:
+                    maybeStartHeaders(input);
+                    break;
+                case STATUS_LINE_END_CR:
+                    maybeEndHeaders(input);
+                    break;
+                case HEADER:
+                    readResumeHeader(input);
+                    break;
+                // fallthrough
+                case HEADER_FOUND_CR:
+                case HEADER_FOUND_LF:
+                    resumeOrLF(input);
+                    break;
+                case HEADER_FOUND_CR_LF:
+                    resumeOrSecondCR(input);
+                    break;
+                case HEADER_FOUND_CR_LF_CR:
+                    resumeOrEndHeaders(input);
+                    break;
+                default:
+                    throw new InternalError(
+                            "Unexpected state: " + String.valueOf(state));
+            }
+        }
+
+        return state == State.FINISHED;
+    }
+
+    private void readResumeStatusLine(ByteBuffer input) {
+        char c = 0;
+        while (input.hasRemaining() && (c =(char)input.get()) != CR) {
+            sb.append(c);
+        }
+
+        if (c == CR) {
+            state = State.STATUS_LINE_FOUND_CR;
+        }
+    }
+
+    private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
+        char c = (char)input.get();
+        if (c != LF) {
+            throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
+                                    c, sb.toString());
+        }
+
+        statusLine = sb.toString();
+        sb = new StringBuilder();
+        if (!statusLine.startsWith("HTTP/1.")) {
+            throw protocolException("Invalid status line: \"%s\"", statusLine);
+        }
+        if (statusLine.length() < 12) {
+            throw protocolException("Invalid status line: \"%s\"", statusLine);
+        }
+        responseCode = Integer.parseInt(statusLine.substring(9, 12));
+
+        state = State.STATUS_LINE_END;
+    }
+
+    private void maybeStartHeaders(ByteBuffer input) {
+        assert state == State.STATUS_LINE_END;
+        assert sb.length() == 0;
+        char c = (char)input.get();
+        if (c == CR) {
+            state = State.STATUS_LINE_END_CR;
+        } else {
+            sb.append(c);
+            state = State.HEADER;
+        }
+    }
+
+    private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
+        assert state == State.STATUS_LINE_END_CR;
+        assert sb.length() == 0;
+        char c = (char)input.get();
+        if (c == LF) {
+            headers = ImmutableHeaders.of(privateMap);
+            privateMap = null;
+            state = State.FINISHED;  // no headers
+        } else {
+            throw protocolException("Unexpected \"%s\", after status-line CR", c);
+        }
+    }
+
+    private void readResumeHeader(ByteBuffer input) {
+        assert state == State.HEADER;
+        assert input.hasRemaining();
+        while (input.hasRemaining()) {
+            char c = (char)input.get();
+            if (c == CR) {
+                state = State.HEADER_FOUND_CR;
+                break;
+            } else if (c == LF) {
+                state = State.HEADER_FOUND_LF;
+                break;
+            }
+
+            if (c == HT)
+                c = SP;
+            sb.append(c);
+        }
+    }
+
+    private void addHeaderFromString(String headerString) {
+        assert sb.length() == 0;
+        int idx = headerString.indexOf(':');
+        if (idx == -1)
+            return;
+        String name = headerString.substring(0, idx).trim();
+        if (name.isEmpty())
+            return;
+        String value = headerString.substring(idx + 1, headerString.length()).trim();
+
+        privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
+                                   k -> new ArrayList<>()).add(value);
+    }
+
+    private void resumeOrLF(ByteBuffer input) {
+        assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
+        char c = (char)input.get();
+        if (c == LF && state == State.HEADER_FOUND_CR) {
+            String headerString = sb.toString();
+            sb = new StringBuilder();
+            addHeaderFromString(headerString);
+            state = State.HEADER_FOUND_CR_LF;
+        } else if (c == SP || c == HT) {
+            sb.append(SP); // parity with MessageHeaders
+            state = State.HEADER;
+        } else {
+            sb = new StringBuilder();
+            sb.append(c);
+            state = State.HEADER;
+        }
+    }
+
+    private void resumeOrSecondCR(ByteBuffer input) {
+        assert state == State.HEADER_FOUND_CR_LF;
+        assert sb.length() == 0;
+        char c = (char)input.get();
+        if (c == CR) {
+            state = State.HEADER_FOUND_CR_LF_CR;
+        } else {
+            sb.append(c);
+            state = State.HEADER;
+        }
+    }
+
+    private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
+        assert state == State.HEADER_FOUND_CR_LF_CR;
+        char c = (char)input.get();
+        if (c == LF) {
+            state = State.FINISHED;
+            headers = ImmutableHeaders.of(privateMap);
+            privateMap = null;
+        } else {
+            throw protocolException("Unexpected \"%s\", after CR LF CR", c);
+        }
+    }
+
+    private ProtocolException protocolException(String format, Object... args) {
+        return new ProtocolException(format(format, args));
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -26,97 +26,76 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.URI;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.net.InetSocketAddress;
-import jdk.incubator.http.HttpConnection.Mode;
-import java.nio.charset.StandardCharsets;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
+import java.util.Objects;
 import java.util.concurrent.Flow;
 
+import jdk.incubator.http.Http1Exchange.Http1BodySubscriber;
 import jdk.incubator.http.internal.common.HttpHeadersImpl;
 import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
 
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
 /**
- *  A HTTP/1.1 request.
- *
- * send() -> Writes the request + body to the given channel, in one blocking
- * operation.
+ *  An HTTP/1.1 request.
  */
 class Http1Request {
-    final HttpClientImpl client;
-    final HttpRequestImpl request;
-    final HttpConnection chan;
-    // Multiple buffers are used to hold different parts of request
-    // See line 206 and below for description
-    final ByteBuffer[] buffers;
-    final HttpRequest.BodyProcessor requestProc;
-    final HttpHeaders userHeaders;
-    final HttpHeadersImpl systemHeaders;
-    boolean streaming;
-    long contentLength;
-    final CompletableFuture<Void> cf;
+    private final HttpClientImpl client;
+    private final HttpRequestImpl request;
+    private final Http1Exchange<?> http1Exchange;
+    private final HttpConnection connection;
+    private final HttpRequest.BodyPublisher requestPublisher;
+    private final HttpHeaders userHeaders;
+    private final HttpHeadersImpl systemHeaders;
+    private volatile boolean streaming;
+    private volatile long contentLength;
 
-    Http1Request(HttpRequestImpl request, HttpClientImpl client, HttpConnection connection)
+    Http1Request(HttpRequestImpl request,
+                 HttpClientImpl client,
+                 Http1Exchange<?> http1Exchange)
         throws IOException
     {
         this.client = client;
         this.request = request;
-        this.chan = connection;
-        buffers = new ByteBuffer[5]; // TODO: check
-        this.requestProc = request.requestProcessor;
+        this.http1Exchange = http1Exchange;
+        this.connection = http1Exchange.connection();
+        this.requestPublisher = request.requestPublisher;  // may be null
         this.userHeaders = request.getUserHeaders();
         this.systemHeaders = request.getSystemHeaders();
-        this.cf = new MinimalFuture<>();
-    }
-
-    private void logHeaders() throws IOException {
-        StringBuilder sb = new StringBuilder(256);
-        sb.append("REQUEST HEADERS:\n");
-        Log.dumpHeaders(sb, "    ", systemHeaders);
-        Log.dumpHeaders(sb, "    ", userHeaders);
-        Log.logHeaders(sb.toString());
-    }
-
-    private void dummy(long x) {
-        // not used in this class
     }
 
-    private void collectHeaders0() throws IOException {
+    private void logHeaders(String completeHeaders) {
         if (Log.headers()) {
-            logHeaders();
+            //StringBuilder sb = new StringBuilder(256);
+            //sb.append("REQUEST HEADERS:\n");
+            //Log.dumpHeaders(sb, "    ", systemHeaders);
+            //Log.dumpHeaders(sb, "    ", userHeaders);
+            //Log.logHeaders(sb.toString());
+
+            String s = completeHeaders.replaceAll("\r\n", "\n");
+            Log.logHeaders("REQUEST HEADERS:\n" + s);
         }
-        StringBuilder sb = new StringBuilder(256);
-        collectHeaders1(sb, request, systemHeaders);
-        collectHeaders1(sb, request, userHeaders);
-        sb.append("\r\n");
-        String headers = sb.toString();
-        buffers[1] = ByteBuffer.wrap(headers.getBytes(StandardCharsets.US_ASCII));
     }
 
-    private void collectHeaders1(StringBuilder sb,
-                                 HttpRequestImpl request,
-                                 HttpHeaders headers)
-        throws IOException
-    {
-        Map<String,List<String>> h = headers.map();
-        Set<Map.Entry<String,List<String>>> entries = h.entrySet();
+    private void collectHeaders0(StringBuilder sb) {
+        collectHeaders1(sb, systemHeaders);
+        collectHeaders1(sb, userHeaders);
+        sb.append("\r\n");
+    }
 
-        for (Map.Entry<String,List<String>> entry : entries) {
+    private void collectHeaders1(StringBuilder sb, HttpHeaders headers) {
+        for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
             String key = entry.getKey();
             List<String> values = entry.getValue();
             for (String value : values) {
-                sb.append(key)
-                  .append(": ")
-                  .append(value)
-                  .append("\r\n");
+                sb.append(key).append(": ").append(value).append("\r\n");
             }
         }
     }
@@ -182,24 +161,6 @@
         return uri == null? authorityString(request.authority()) : uri.toString();
     }
 
-    void sendHeadersOnly() throws IOException {
-        collectHeaders();
-        chan.write(buffers, 0, 2);
-    }
-
-    void sendRequest() throws IOException {
-        collectHeaders();
-        chan.configureMode(Mode.BLOCKING);
-        if (contentLength == 0) {
-            chan.write(buffers, 0, 2);
-        } else if (contentLength > 0) {
-            writeFixedContent(true);
-        } else {
-            writeStreamedContent(true);
-        }
-        setFinished();
-    }
-
     private boolean finished;
 
     synchronized boolean finished() {
@@ -210,7 +171,7 @@
         finished = true;
     }
 
-    private void collectHeaders() throws IOException {
+    List<ByteBuffer> headers() {
         if (Log.requests() && request != null) {
             Log.logRequest(request.toString());
         }
@@ -220,250 +181,183 @@
           .append(' ')
           .append(uriString)
           .append(" HTTP/1.1\r\n");
-        String cmd = sb.toString();
 
-        buffers[0] = ByteBuffer.wrap(cmd.getBytes(StandardCharsets.US_ASCII));
         URI uri = request.uri();
         if (uri != null) {
             systemHeaders.setHeader("Host", hostString());
         }
-        if (request == null) {
-            // this is not a user request. No content
+        if (request == null || requestPublisher == null) {
+            // Not a user request, or maybe a method, e.g. GET, with no body.
             contentLength = 0;
         } else {
-            contentLength = requestProc.contentLength();
+            contentLength = requestPublisher.contentLength();
         }
 
         if (contentLength == 0) {
             systemHeaders.setHeader("Content-Length", "0");
-            collectHeaders0();
         } else if (contentLength > 0) {
-            /* [0] request line [1] headers [2] body  */
-            systemHeaders.setHeader("Content-Length",
-                                    Integer.toString((int) contentLength));
+            systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
             streaming = false;
-            collectHeaders0();
-            buffers[2] = getBuffer();
         } else {
-            /* Chunked:
-             *
-             * [0] request line [1] headers [2] chunk header [3] chunk data [4]
-             * final chunk header and trailing CRLF of previous chunks
-             *
-             * 2,3,4 used repeatedly */
             streaming = true;
             systemHeaders.setHeader("Transfer-encoding", "chunked");
-            collectHeaders0();
-            buffers[3] = getBuffer();
         }
+        collectHeaders0(sb);
+        String hs = sb.toString();
+        logHeaders(hs);
+        ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
+        return List.of(b);
     }
 
-    private ByteBuffer getBuffer() {
-        return ByteBuffer.allocate(Utils.BUFSIZE);
+    Http1BodySubscriber continueRequest()  {
+        Http1BodySubscriber subscriber;
+        if (streaming) {
+            subscriber = new StreamSubscriber();
+            requestPublisher.subscribe(subscriber);
+        } else {
+            if (contentLength == 0)
+                return null;
+
+            subscriber = new FixedContentSubscriber();
+            requestPublisher.subscribe(subscriber);
+        }
+        return subscriber;
     }
 
-    // The following two methods used by Http1Exchange to handle expect continue
-
-    void continueRequest() throws IOException {
-        if (streaming) {
-            writeStreamedContent(false);
-        } else {
-            writeFixedContent(false);
-        }
-        setFinished();
-    }
-
-    class StreamSubscriber implements Flow.Subscriber<ByteBuffer> {
-        volatile Flow.Subscription subscription;
-        volatile boolean includeHeaders;
-
-        StreamSubscriber(boolean includeHeaders) {
-            this.includeHeaders = includeHeaders;
-        }
+    class StreamSubscriber extends Http1BodySubscriber {
 
         @Override
         public void onSubscribe(Flow.Subscription subscription) {
             if (this.subscription != null) {
-                throw new IllegalStateException("already subscribed");
+                Throwable t = new IllegalStateException("already subscribed");
+                http1Exchange.appendToOutgoing(t);
+            } else {
+                this.subscription = subscription;
             }
-            this.subscription = subscription;
-            subscription.request(1);
         }
 
         @Override
         public void onNext(ByteBuffer item) {
-            int startbuf, nbufs;
-
-            if (cf.isDone()) {
-                throw new IllegalStateException("subscription already completed");
-            }
-
-            if (includeHeaders) {
-                startbuf = 0;
-                nbufs = 5;
+            Objects.requireNonNull(item);
+            if (complete) {
+                Throwable t = new IllegalStateException("subscription already completed");
+                http1Exchange.appendToOutgoing(t);
             } else {
-                startbuf = 2;
-                nbufs = 3;
-            }
-            int chunklen = item.remaining();
-            buffers[3] = item;
-            buffers[2] = getHeader(chunklen);
-            buffers[4] = CRLF_BUFFER();
-            try {
-                chan.write(buffers, startbuf, nbufs);
-            } catch (IOException e) {
-                subscription.cancel();
-                cf.completeExceptionally(e);
-            }
-            includeHeaders = false;
-            subscription.request(1);
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            if (cf.isDone()) {
-                return;
-            }
-            subscription.cancel();
-            cf.completeExceptionally(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            if (cf.isDone()) {
-                throw new IllegalStateException("subscription already completed");
-            }
-            buffers[3] = EMPTY_CHUNK_HEADER();
-            buffers[4] = CRLF_BUFFER();
-            try {
-                chan.write(buffers, 3, 2);
-            } catch (IOException ex) {
-                cf.completeExceptionally(ex);
-                return;
-            }
-            cf.complete(null);
-        }
-    }
-
-    private void waitForCompletion() throws IOException {
-        try {
-            cf.join();
-        } catch (CompletionException e) {
-            throw Utils.getIOException(e);
-        }
-    }
-
-    /* Entire request is sent, or just body only  */
-    private void writeStreamedContent(boolean includeHeaders)
-        throws IOException
-    {
-        StreamSubscriber subscriber = new StreamSubscriber(includeHeaders);
-        requestProc.subscribe(subscriber);
-        waitForCompletion();
-    }
-
-    class FixedContentSubscriber implements Flow.Subscriber<ByteBuffer>
-    {
-        volatile Flow.Subscription subscription;
-        volatile boolean includeHeaders;
-        volatile long contentWritten = 0;
-
-        FixedContentSubscriber(boolean includeHeaders) {
-            this.includeHeaders = includeHeaders;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            if (this.subscription != null) {
-                throw new IllegalStateException("already subscribed");
-            }
-            this.subscription = subscription;
-            subscription.request(1);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            int startbuf, nbufs;
-            long headersLength;
-
-            if (includeHeaders) {
-                startbuf = 0;
-                nbufs = 3;
-                headersLength = buffers[0].remaining() + buffers[1].remaining();
-            } else {
-                startbuf = 2;
-                nbufs = 1;
-                headersLength = 0;
-            }
-            buffers[2] = item;
-            try {
-                long writing = buffers[2].remaining() + headersLength;
-                contentWritten += buffers[2].remaining();
-                chan.checkWrite(writing, buffers, startbuf, nbufs);
-
-                if (contentWritten > contentLength) {
-                    String msg = "Too many bytes in request body. Expected: " +
-                        Long.toString(contentLength) + " Sent: " +
-                        Long.toString(contentWritten);
-                    throw new IOException(msg);
-                }
-                subscription.request(1);
-            } catch (IOException e) {
-                subscription.cancel();
-                cf.completeExceptionally(e);
+                int chunklen = item.remaining();
+                ArrayList<ByteBuffer> l = new ArrayList<>(3);
+                l.add(getHeader(chunklen));
+                l.add(item);
+                l.add(ByteBuffer.wrap(CRLF));
+                http1Exchange.appendToOutgoing(l);
             }
         }
 
         @Override
         public void onError(Throwable throwable) {
-            if (cf.isDone()) {
+            if (complete)
                 return;
-            }
+
             subscription.cancel();
-            cf.completeExceptionally(throwable);
+            http1Exchange.appendToOutgoing(throwable);
         }
 
         @Override
         public void onComplete() {
-            if (cf.isDone()) {
-                throw new IllegalStateException("subscription already completed");
-            }
+            if (complete) {
+                Throwable t = new IllegalStateException("subscription already completed");
+                http1Exchange.appendToOutgoing(t);
+            } else {
+                ArrayList<ByteBuffer> l = new ArrayList<>(2);
+                l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
+                l.add(ByteBuffer.wrap(CRLF));
+                complete = true;
+                //setFinished();
+                http1Exchange.appendToOutgoing(l);
+                http1Exchange.appendToOutgoing(COMPLETED);
+                setFinished();  // TODO: before or after,? does it matter?
 
-            if (contentLength > contentWritten) {
-                subscription.cancel();
-                Exception e = new IOException("Too few bytes returned by the processor");
-                cf.completeExceptionally(e);
-            } else {
-                cf.complete(null);
             }
         }
     }
 
-    /* Entire request is sent, or just body only */
-    private void writeFixedContent(boolean includeHeaders)
-            throws IOException {
-        if (contentLength == 0) {
-            return;
+    class FixedContentSubscriber extends Http1BodySubscriber {
+
+        private volatile long contentWritten;
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            if (this.subscription != null) {
+                Throwable t = new IllegalStateException("already subscribed");
+                http1Exchange.appendToOutgoing(t);
+            } else {
+                this.subscription = subscription;
+            }
         }
-        FixedContentSubscriber subscriber = new FixedContentSubscriber(includeHeaders);
-        requestProc.subscribe(subscriber);
-        waitForCompletion();
+
+        @Override
+        public void onNext(ByteBuffer item) {
+            debug.log(Level.DEBUG, "onNext");
+            Objects.requireNonNull(item);
+            if (complete) {
+                Throwable t = new IllegalStateException("subscription already completed");
+                http1Exchange.appendToOutgoing(t);
+            } else {
+                long writing = item.remaining();
+                long written = (contentWritten += writing);
+
+                if (written > contentLength) {
+                    subscription.cancel();
+                    String msg = connection.getConnectionFlow()
+                                  + " [" + Thread.currentThread().getName() +"] "
+                                  + "Too many bytes in request body. Expected: "
+                                  + contentLength + ", got: " + written;
+                    http1Exchange.appendToOutgoing(new IOException(msg));
+                } else {
+                    http1Exchange.appendToOutgoing(List.of(item));
+                }
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            debug.log(Level.DEBUG, "onError");
+            if (complete)  // TODO: error?
+                return;
+
+            subscription.cancel();
+            http1Exchange.appendToOutgoing(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            debug.log(Level.DEBUG, "onComplete");
+            if (complete) {
+                Throwable t = new IllegalStateException("subscription already completed");
+                http1Exchange.appendToOutgoing(t);
+            } else {
+                complete = true;
+                long written = contentWritten;
+                if (contentLength > written) {
+                    subscription.cancel();
+                    Throwable t = new IOException(connection.getConnectionFlow()
+                                         + " [" + Thread.currentThread().getName() +"] "
+                                         + "Too few bytes returned by the publisher ("
+                                                  + written + "/"
+                                                  + contentLength + ")");
+                    http1Exchange.appendToOutgoing(t);
+                } else {
+                    http1Exchange.appendToOutgoing(COMPLETED);
+                }
+            }
+        }
     }
 
     private static final byte[] CRLF = {'\r', '\n'};
     private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
 
-    private ByteBuffer CRLF_BUFFER() {
-        return ByteBuffer.wrap(CRLF);
-    }
-
-    private ByteBuffer EMPTY_CHUNK_HEADER() {
-        return ByteBuffer.wrap(EMPTY_CHUNK_BYTES);
-    }
-
-    /* Returns a header for a particular chunk size */
-    private static ByteBuffer getHeader(int size){
-        String hexStr =  Integer.toHexString(size);
+    /** Returns a header for a particular chunk size */
+    private static ByteBuffer getHeader(int size) {
+        String hexStr = Integer.toHexString(size);
         byte[] hexBytes = hexStr.getBytes(US_ASCII);
         byte[] header = new byte[hexStr.length()+2];
         System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
@@ -471,4 +365,8 @@
         header[hexBytes.length+1] = CRLF[1];
         return ByteBuffer.wrap(header);
     }
+
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::toString, DEBUG);
+
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,16 +25,23 @@
 
 package jdk.incubator.http;
 
-import java.io.IOException;
+import java.io.EOFException;
+import java.lang.System.Logger.Level;
 import java.nio.ByteBuffer;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import jdk.incubator.http.ResponseContent.BodyParser;
 import jdk.incubator.http.internal.common.Log;
 import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
 
 /**
- * Handles a HTTP/1.1 response in two blocking calls. readHeaders() and
- * readBody(). There can be more than one of these per Http exchange.
+ * Handles a HTTP/1.1 response (headers + body).
+ * There can be more than one of these per Http exchange.
  */
 class Http1Response<T> {
 
@@ -42,48 +49,72 @@
     private final HttpRequestImpl request;
     private Response response;
     private final HttpConnection connection;
-    private ResponseHeaders headers;
+    private HttpHeaders headers;
     private int responseCode;
-    private ByteBuffer buffer;
     private final Http1Exchange<T> exchange;
     private final boolean redirecting; // redirecting
     private boolean return2Cache; // return connection to cache when finished
+    private final HeadersReader headersReader; // used to read the headers
+    private final BodyReader bodyReader; // used to read the body
+    private final Http1AsyncReceiver asyncReceiver;
+    private volatile boolean reading;
+    private volatile EOFException eof;
 
-    Http1Response(HttpConnection conn, Http1Exchange<T> exchange) {
+    // Revisit: can we get rid of this?
+    static enum State {INITIAL, READING_HEADERS, READING_BODY, DONE}
+    private volatile State readProgress = State.INITIAL;
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
+
+
+    Http1Response(HttpConnection conn,
+                  Http1Exchange<T> exchange,
+                  Http1AsyncReceiver asyncReceiver) {
+        this.readProgress = State.INITIAL;
         this.request = exchange.request();
         this.exchange = exchange;
         this.connection = conn;
         this.redirecting = false;
-        buffer = exchange.getBuffer();
+        this.asyncReceiver = asyncReceiver;
+        headersReader = new HeadersReader(this::advance);
+        bodyReader = new BodyReader(this::advance);
     }
 
-    @SuppressWarnings("unchecked")
-    public void readHeaders() throws IOException {
-        String statusline = readStatusLine();
-        if (statusline == null) {
-            if (Log.errors()) {
-                Log.logError("Connection closed. Retry");
-            }
-            connection.close();
-            // connection was closed
-            throw new IOException("Connection closed");
-        }
-        if (!statusline.startsWith("HTTP/1.")) {
-            throw new IOException("Invalid status line: " + statusline);
+   public CompletableFuture<Response> readHeadersAsync(Executor executor) {
+        debug.log(Level.DEBUG, () -> "Reading Headers: (remaining: "
+                + asyncReceiver.remaining() +") "  + readProgress);
+        // with expect continue we will resume reading headers + body.
+        asyncReceiver.unsubscribe(bodyReader);
+        bodyReader.reset();
+        Http1HeaderParser hd = new Http1HeaderParser();
+        readProgress = State.READING_HEADERS;
+        headersReader.start(hd);
+        asyncReceiver.subscribe(headersReader);
+        CompletableFuture<State> cf = headersReader.completion();
+        assert cf != null : "parsing not started";
+
+        Function<State, Response> lambda = (State completed) -> {
+                assert completed == State.READING_HEADERS;
+                debug.log(Level.DEBUG, () ->
+                            "Reading Headers: creating Response object;"
+                            + " state is now " + readProgress);
+                asyncReceiver.unsubscribe(headersReader);
+                responseCode = hd.responseCode();
+                headers = hd.headers();
+
+                response = new Response(request,
+                                        exchange.getExchange(),
+                                        headers,
+                                        responseCode,
+                                        HTTP_1_1);
+                return response;
+            };
+
+        if (executor != null) {
+            return cf.thenApplyAsync(lambda, executor);
+        } else {
+            return cf.thenApply(lambda);
         }
-        if (Log.trace()) {
-            Log.logTrace("Statusline: {0}", statusline);
-        }
-        char c = statusline.charAt(7);
-        responseCode = Integer.parseInt(statusline.substring(9, 12));
-
-        headers = new ResponseHeaders(connection, buffer);
-        if (Log.headers()) {
-            logHeaders(headers);
-        }
-        response = new Response(
-                request, exchange.getExchange(),
-                headers, responseCode, HTTP_1_1);
     }
 
     private boolean finished;
@@ -96,10 +127,6 @@
         return finished;
     }
 
-    ByteBuffer getBuffer() {
-        return buffer;
-    }
-
     int fixupContentLen(int clen) {
         if (request.method().equalsIgnoreCase("HEAD")) {
             return 0;
@@ -114,62 +141,113 @@
         return clen;
     }
 
-    public CompletableFuture<T> readBody(
-            HttpResponse.BodyProcessor<T> p,
-            boolean return2Cache,
-            Executor executor) {
-        final BlockingPushPublisher<ByteBuffer> publisher = new BlockingPushPublisher<>();
-        return readBody(p, return2Cache, publisher, executor);
-    }
-
-    private CompletableFuture<T> readBody(
-            HttpResponse.BodyProcessor<T> p,
-            boolean return2Cache,
-            AbstractPushPublisher<ByteBuffer> publisher,
-            Executor executor) {
+    public CompletableFuture<T> readBody(HttpResponse.BodySubscriber<T> p,
+                                         boolean return2Cache,
+                                         Executor executor) {
         this.return2Cache = return2Cache;
-        final jdk.incubator.http.HttpResponse.BodyProcessor<T> pusher = p;
+        final HttpResponse.BodySubscriber<T> pusher = p;
         final CompletableFuture<T> cf = p.getBody().toCompletableFuture();
 
-        int clen0;
-        try {
-            clen0 = headers.getContentLength();
-        } catch (IOException ex) {
-            cf.completeExceptionally(ex);
-            return cf;
-        }
+        int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
+
         final int clen = fixupContentLen(clen0);
 
+        // expect-continue reads headers and body twice.
+        // if we reach here, we must reset the headersReader state.
+        asyncReceiver.unsubscribe(headersReader);
+        headersReader.reset();
+
         executor.execute(() -> {
             try {
+                HttpClientImpl client = connection.client();
                 content = new ResponseContent(
                         connection, clen, headers, pusher,
-                        publisher.asDataConsumer(),
-                        (t -> {
-                            publisher.acceptError(t);
-                            connection.close();
-                            cf.completeExceptionally(t);
-                        }),
-                        () -> onFinished()
+                        this::onFinished
                 );
-                publisher.subscribe(p);
                 if (cf.isCompletedExceptionally()) {
                     // if an error occurs during subscription
                     connection.close();
                     return;
                 }
-                content.pushBody(buffer);
+                // increment the reference count on the HttpClientImpl
+                // to prevent the SelectorManager thread from exiting until
+                // the body is fully read.
+                client.reference();
+                bodyReader.start(content.getBodyParser(
+                    (t) -> {
+                        try {
+                            if (t != null) {
+                                pusher.onError(t);
+                                connection.close();
+                                if (!cf.isDone())
+                                    cf.completeExceptionally(t);
+                            }
+                        } finally {
+                            // decrement the reference count on the HttpClientImpl
+                            // to allow the SelectorManager thread to exit if no
+                            // other operation is pending and the facade is no
+                            // longer referenced.
+                            client.unreference();
+                            bodyReader.onComplete(t);
+                        }
+                    }));
+                CompletableFuture<State> bodyReaderCF = bodyReader.completion();
+                asyncReceiver.subscribe(bodyReader);
+                assert bodyReaderCF != null : "parsing not started";
+                // Make sure to keep a reference to asyncReceiver from
+                // within this
+                CompletableFuture<?> trailingOp = bodyReaderCF.whenComplete((s,t) ->  {
+                    t = Utils.getCompletionCause(t);
+                    try {
+                        if (t != null) {
+                            debug.log(Level.DEBUG, () ->
+                                    "Finished reading body: " + s);
+                            assert s == State.READING_BODY;
+                        }
+                        if (t != null && !cf.isDone()) {
+                            pusher.onError(t);
+                            cf.completeExceptionally(t);
+                        }
+                    } catch (Throwable x) {
+                        // not supposed to happen
+                        asyncReceiver.onReadError(x);
+                    }
+                });
+                connection.addTrailingOperation(trailingOp);
             } catch (Throwable t) {
-                cf.completeExceptionally(t);
+               debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
+                try {
+                    if (!cf.isDone()) {
+                        pusher.onError(t);
+                        cf.completeExceptionally(t);
+                    }
+                } finally {
+                    asyncReceiver.onReadError(t);
+                }
             }
         });
-        return cf;
+        BiConsumer<Object,Throwable> whenComplete = (r,t) -> {
+            if (t != null) {
+                asyncReceiver.unsubscribe(bodyReader);
+                bodyReader.reset();
+            }
+        };
+        return cf.whenComplete(whenComplete);
     }
 
+
     private void onFinished() {
+        asyncReceiver.clear();
         if (return2Cache) {
-            Log.logTrace("Returning connection to the pool: {0}", connection);
-            connection.returnToCache(headers);
+            Log.logTrace("Attempting to return connection to the pool: {0}", connection);
+            reading = false;
+            // TODO: need to do something here?
+            // connection.setAsyncCallbacks(null, null, null);
+
+            // don't return the connection to the cache if EOF happened.
+            debug.log(Level.DEBUG, () -> connection.getConnectionFlow()
+                                   + ": return to HTTP/1.1 pool");
+            connection.closeOrReturnToCache(eof == null ? headers : null);
         }
     }
 
@@ -198,47 +276,251 @@
     static final char CR = '\r';
     static final char LF = '\n';
 
-    private int obtainBuffer() throws IOException {
-        int n = buffer.remaining();
+// ================ Support for plugging into AsyncConnection =================
+// ============================================================================
+
+    // Callback: Error receiver: Consumer of Throwable.
+    void onReadError(Throwable t) {
+        Log.logError(t);
+        Receiver<?> receiver = receiver(readProgress);
+        if (t instanceof EOFException) {
+            debug.log(Level.DEBUG, "onReadError: received EOF");
+            eof = (EOFException) t;
+        }
+        CompletableFuture<?> cf = receiver == null ? null : receiver.completion();
+        debug.log(Level.DEBUG, () -> "onReadError: cf is "
+                + (cf == null  ? "null"
+                : (cf.isDone() ? "already completed"
+                               : "not yet completed")));
+        if (cf != null && !cf.isDone()) cf.completeExceptionally(t);
+        else { debug.log(Level.DEBUG, "onReadError", t); }
+        debug.log(Level.DEBUG, () -> "closing connection: cause is " + t);
+        connection.close();
+    }
+
+    // ========================================================================
+
+    private State advance(State previous) {
+        assert readProgress == previous;
+        switch(previous) {
+            case READING_HEADERS:
+                asyncReceiver.unsubscribe(headersReader);
+                return readProgress = State.READING_BODY;
+            case READING_BODY:
+                asyncReceiver.unsubscribe(bodyReader);
+                return readProgress = State.DONE;
+            default:
+                throw new InternalError("can't advance from " + previous);
+        }
+    }
 
-        if (n == 0) {
-            buffer = connection.read();
-            if (buffer == null) {
-                return -1;
-            }
-            n = buffer.remaining();
+    Receiver<?> receiver(State state) {
+        switch(state) {
+            case READING_HEADERS: return headersReader;
+            case READING_BODY: return bodyReader;
+            default: return null;
         }
-        return n;
+
+    }
+
+    static abstract class Receiver<T>
+            implements Http1AsyncReceiver.Http1AsyncDelegate {
+        abstract void start(T parser);
+        abstract CompletableFuture<State> completion();
+        // accepts a buffer from upstream.
+        // this should be implemented as a simple call to
+        // accept(ref, parser, cf)
+        public abstract boolean tryAsyncReceive(ByteBuffer buffer);
+        public abstract void onReadError(Throwable t);
+        // handle a byte buffer received from upstream.
+        // this method should set the value of Http1Response.buffer
+        // to ref.get() before beginning parsing.
+        abstract void handle(ByteBuffer buf, T parser,
+                             CompletableFuture<State> cf);
+        // resets this objects state so that it can be reused later on
+        // typically puts the reference to parser and completion to null
+        abstract void reset();
+
+        // accepts a byte buffer received from upstream
+        // returns true if the buffer is fully parsed and more data can
+        // be accepted, false otherwise.
+        final boolean accept(ByteBuffer buf, T parser,
+                CompletableFuture<State> cf) {
+            if (cf == null || parser == null || cf.isDone()) return false;
+            handle(buf, parser, cf);
+            return !cf.isDone();
+        }
+        public abstract void onSubscribe(AbstractSubscription s);
+        public abstract AbstractSubscription subscription();
+
     }
 
-    String readStatusLine() throws IOException {
-        boolean cr = false;
-        StringBuilder statusLine = new StringBuilder(128);
-        while ((obtainBuffer()) != -1) {
-            byte[] buf = buffer.array();
-            int offset = buffer.position();
-            int len = buffer.limit() - offset;
+    // Invoked with each new ByteBuffer when reading headers...
+    final class HeadersReader extends Receiver<Http1HeaderParser> {
+        final Consumer<State> onComplete;
+        volatile Http1HeaderParser parser;
+        volatile CompletableFuture<State> cf;
+        volatile long count; // bytes parsed (for debug)
+        volatile AbstractSubscription subscription;
+
+        HeadersReader(Consumer<State> onComplete) {
+            this.onComplete = onComplete;
+        }
+
+        @Override
+        public AbstractSubscription subscription() {
+            return subscription;
+        }
+
+        @Override
+        public void onSubscribe(AbstractSubscription s) {
+            this.subscription = s;
+            s.request(1);
+        }
 
-            for (int i = 0; i < len; i++) {
-                char c = (char) buf[i+offset];
+        @Override
+        void reset() {
+            cf = null;
+            parser = null;
+            count = 0;
+            subscription = null;
+        }
+
+        // Revisit: do we need to support restarting?
+        @Override
+        final void start(Http1HeaderParser hp) {
+            count = 0;
+            cf = new MinimalFuture<>();
+            parser = hp;
+        }
+
+        @Override
+        CompletableFuture<State> completion() {
+            return cf;
+        }
+
+        @Override
+        public final boolean tryAsyncReceive(ByteBuffer ref) {
+            boolean hasDemand = subscription.demand().tryDecrement();
+            assert hasDemand;
+            boolean needsMore = accept(ref, parser, cf);
+            if (needsMore) subscription.request(1);
+            return needsMore;
+        }
+
+        @Override
+        public final void onReadError(Throwable t) {
+            Http1Response.this.onReadError(t);
+        }
 
-                if (cr) {
-                    if (c == LF) {
-                        buffer.position(i + 1 + offset);
-                        return statusLine.toString();
-                    } else {
-                        throw new IOException("invalid status line");
-                    }
+        @Override
+        final void handle(ByteBuffer b,
+                          Http1HeaderParser parser,
+                          CompletableFuture<State> cf) {
+            assert cf != null : "parsing not started";
+            assert parser != null : "no parser";
+            try {
+                count += b.remaining();
+                debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+                        + "/" + b.capacity() + " bytes to header parser");
+                if (parser.parse(b)) {
+                    count -= b.remaining();
+                    debug.log(Level.DEBUG, () ->
+                            "Parsing headers completed. bytes=" + count);
+                    onComplete.accept(State.READING_HEADERS);
+                    cf.complete(State.READING_HEADERS);
                 }
-                if (c == CR) {
-                    cr = true;
-                } else {
-                    statusLine.append(c);
+            } catch (Throwable t) {
+                debug.log(Level.DEBUG,
+                        () -> "Header parser failed to handle buffer: " + t);
+                cf.completeExceptionally(t);
+            }
+        }
+    }
+
+    // Invoked with each new ByteBuffer when reading bodies...
+    final class BodyReader extends Receiver<BodyParser> {
+        final Consumer<State> onComplete;
+        volatile BodyParser parser;
+        volatile CompletableFuture<State> cf;
+        volatile AbstractSubscription subscription;
+        BodyReader(Consumer<State> onComplete) {
+            this.onComplete = onComplete;
+        }
+
+        @Override
+        void reset() {
+            parser = null;
+            cf = null;
+            subscription = null;
+        }
+
+        // Revisit: do we need to support restarting?
+        @Override
+        final void start(BodyParser parser) {
+            cf = new MinimalFuture<>();
+            this.parser = parser;
+        }
+
+        @Override
+        CompletableFuture<State> completion() {
+            return cf;
+        }
+
+        @Override
+        public final boolean tryAsyncReceive(ByteBuffer b) {
+            return accept(b, parser, cf);
+        }
+
+        @Override
+        public final void onReadError(Throwable t) {
+            Http1Response.this.onReadError(t);
+        }
+
+        @Override
+        public AbstractSubscription subscription() {
+            return subscription;
+        }
+
+        @Override
+        public void onSubscribe(AbstractSubscription s) {
+            this.subscription = s;
+            parser.onSubscribe(s);
+        }
+
+        @Override
+        final void handle(ByteBuffer b,
+                          BodyParser parser,
+                          CompletableFuture<State> cf) {
+            assert cf != null : "parsing not started";
+            assert parser != null : "no parser";
+            try {
+                debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+                        + "/" + b.capacity() + " bytes to body parser");
+                parser.accept(b);
+            } catch (Throwable t) {
+                debug.log(Level.DEBUG,
+                        () -> "Body parser failed to handle buffer: " + t);
+                if (!cf.isDone()) {
+                    cf.completeExceptionally(t);
                 }
             }
-            // unlikely, but possible, that multiple reads required
-            buffer.position(buffer.limit());
         }
-        return null;
+
+        final void onComplete(Throwable closedExceptionally) {
+            if (cf.isDone()) return;
+            if (closedExceptionally != null) {
+                cf.completeExceptionally(closedExceptionally);
+            } else {
+                onComplete.accept(State.READING_BODY);
+                cf.complete(State.READING_BODY);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return super.toString() + "/parser=" + String.valueOf(parser);
+        }
+
     }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -25,17 +25,18 @@
 
 package jdk.incubator.http;
 
-import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.URI;
 import java.util.Base64;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-
+import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
 import jdk.incubator.http.internal.frame.SettingsFrame;
 import static jdk.incubator.http.internal.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
@@ -49,6 +50,10 @@
  */
 class Http2ClientImpl {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final static System.Logger debug =
+            Utils.getDebugLogger("Http2ClientImpl"::toString, DEBUG);
+
     private final HttpClientImpl client;
 
     Http2ClientImpl(HttpClientImpl client) {
@@ -59,13 +64,26 @@
     private final Map<String,Http2Connection> connections = new ConcurrentHashMap<>();
 
     private final Set<String> opening = Collections.synchronizedSet(new HashSet<>());
+    private final Map<String,Set<CompletableFuture<Http2Connection>>> waiting =
+    Collections.synchronizedMap(new HashMap<>());
+
+    private void addToWaiting(String key, CompletableFuture<Http2Connection> cf) {
+        synchronized (waiting) {
+            Set<CompletableFuture<Http2Connection>> waiters = waiting.get(key);
+            if (waiters == null) {
+                waiters = new HashSet<>();
+                waiting.put(key, waiters);
+            }
+            waiters.add(cf);
+        }
+    }
 
     boolean haveConnectionFor(URI uri, InetSocketAddress proxy) {
         return connections.containsKey(Http2Connection.keyFor(uri,proxy));
     }
 
     /**
-     * If a https request then blocks and waits until a connection is opened.
+     * If a https request then async waits until a connection is opened.
      * Returns null if the request is 'http' as a different (upgrade)
      * mechanism is used.
      *
@@ -78,50 +96,59 @@
      * In latter case, when the Http2Connection is connected, putConnection() must
      * be called to store it.
      */
-    Http2Connection getConnectionFor(HttpRequestImpl req)
-            throws IOException, InterruptedException {
+    CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
         URI uri = req.uri();
         InetSocketAddress proxy = req.proxy(client);
         String key = Http2Connection.keyFor(uri, proxy);
-        Http2Connection connection = connections.get(key);
-        if (connection != null) { // fast path if connection already exists
-            return connection;
-        }
+
         synchronized (opening) {
-            while ((connection = connections.get(key)) == null) {
-                if (!req.secure()) {
-                    return null;
-                }
-                if (!opening.contains(key)) {
-                    opening.add(key);
-                    break;
-                } else {
-                    opening.wait();
-                }
+            Http2Connection connection = connections.get(key);
+            if (connection != null) { // fast path if connection already exists
+                return CompletableFuture.completedFuture(connection);
+            }
+
+            if (!req.secure()) {
+                return MinimalFuture.completedFuture(null);
+            }
+
+            if (!opening.contains(key)) {
+                debug.log(Level.DEBUG, "Opening: %s", key);
+                opening.add(key);
+            } else {
+                CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
+                addToWaiting(key, cf);
+                return cf;
             }
         }
-        if (connection != null) {
-            return connection;
-        }
-        // we are opening the connection here blocking until it is done.
-        try {
-            connection = new Http2Connection(req, this);
-        } catch (Throwable t) {
-            synchronized (opening) {
-                opening.remove(key);
-                opening.notifyAll();
-            }
-            throw t;
-        }
-        synchronized (opening) {
-            connections.put(key, connection);
-            opening.remove(key);
-            opening.notifyAll();
-        }
-        return connection;
+        return Http2Connection
+                .createAsync(req, this)
+                .whenComplete((conn, t) -> {
+                    debug.log(Level.DEBUG,
+                            "waking up dependents with created connection");
+                    synchronized (opening) {
+                        Set<CompletableFuture<Http2Connection>> waiters = waiting.remove(key);
+                        debug.log(Level.DEBUG, "Opening completed: %s", key);
+                        opening.remove(key);
+                        final Throwable cause = Utils.getCompletionCause(t);
+                        if (waiters == null) {
+                            debug.log(Level.DEBUG, "no dependent to wake up");
+                            return;
+                        } else if (cause instanceof Http2Connection.ALPNException) {
+                            waiters.forEach((cf1) -> cf1.completeAsync(() -> null,
+                                    client.theExecutor()));
+                        } else if (cause != null) {
+                            debug.log(Level.DEBUG,
+                                    () -> "waking up dependants: failed: " + cause);
+                            waiters.forEach((cf1) -> cf1.completeExceptionally(cause));
+                        } else  {
+                            debug.log(Level.DEBUG, "waking up dependants: succeeded");
+                            waiters.forEach((cf1) -> cf1.completeAsync(() -> conn,
+                                    client.theExecutor()));
+                        }
+                    }
+                });
     }
 
-
     /*
      * TODO: If there isn't a connection to the same destination, then
      * store it. If there is already a connection, then close it
@@ -134,6 +161,16 @@
         connections.remove(c.key());
     }
 
+    void stop() {
+        debug.log(Level.DEBUG, "stopping");
+        connections.values().stream().forEach(this::close);
+        connections.clear();
+    }
+
+    private void close(Http2Connection h2c) {
+        try { h2c.close(); } catch (Throwable t) {}
+    }
+
     HttpClientImpl client() {
         return client;
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,26 +25,32 @@
 
 package jdk.incubator.http;
 
+import java.io.EOFException;
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.URI;
-import jdk.incubator.http.HttpConnection.Mode;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Formatter;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import javax.net.ssl.SSLEngine;
 import jdk.incubator.http.internal.common.*;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
 import jdk.incubator.http.internal.frame.*;
 import jdk.incubator.http.internal.hpack.Encoder;
 import jdk.incubator.http.internal.hpack.Decoder;
@@ -83,11 +89,21 @@
  * stream are provided by calling Stream.incoming().
  */
 class Http2Connection  {
+
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    static final boolean DEBUG_HPACK = Utils.DEBUG_HPACK; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+    final static System.Logger  DEBUG_LOGGER =
+            Utils.getDebugLogger("Http2Connection"::toString, DEBUG);
+    private final System.Logger debugHpack =
+                  Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+    static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);
+
     /*
      *  ByteBuffer pooling strategy for HTTP/2 protocol:
      *
      * In general there are 4 points where ByteBuffers are used:
-     *  - incoming/outgoing frames from/to ByteBufers plus incoming/outgoing encrypted data
+     *  - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing encrypted data
      *    in case of SSL connection.
      *
      * 1. Outgoing frames encoded to ByteBuffers.
@@ -123,10 +139,15 @@
         {
             // if preface is not sent, buffers data in the pending list
             if (!prefaceSent) {
+                debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
+                          buf.get().remaining());
                 synchronized (this) {
                     if (!prefaceSent) {
                         if (pending == null) pending = new ArrayList<>();
                         pending.add(buf);
+                        debug.log(Level.DEBUG, () -> "there are now "
+                              + Utils.remaining(pending.toArray(new ByteBufferReference[0]))
+                              + " bytes buffered waiting for preface to be sent");
                         return false;
                     }
                 }
@@ -143,13 +164,18 @@
             this.pending = null;
             if (pending != null) {
                 // flush pending data
+                debug.log(Level.DEBUG, () -> "Processing buffered data: "
+                      + Utils.remaining(pending.toArray(new ByteBufferReference[0])));
                 for (ByteBufferReference b : pending) {
                     decoder.decode(b);
                 }
             }
-
+            ByteBuffer b = buf.get();
             // push the received buffer to the frames decoder.
-            decoder.decode(buf);
+            if (b != EMPTY_TRIGGER) {
+                debug.log(Level.DEBUG, "Processing %d", buf.get().remaining());
+                decoder.decode(buf);
+            }
             return true;
         }
 
@@ -167,7 +193,9 @@
 
     //-------------------------------------
     final HttpConnection connection;
-    private final HttpClientImpl client;
+    // only keep a strong reference to Http2ClientImpl, which only has
+    // a weak reference on HttpClientImpl, to avoid strong references
+    // from the selector thread to HttpClientImpl (via attachments).
     private final Http2ClientImpl client2;
     private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
     private int nextstreamid;
@@ -186,7 +214,10 @@
      */
     private final WindowController windowController = new WindowController();
     private final FramesController framesController = new FramesController();
+    private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
     final WindowUpdateSender windowUpdater;
+    private volatile Throwable cause;
+    private volatile Supplier<ByteBuffer> initial;
 
     static final int DEFAULT_FRAME_SIZE = 16 * 1024;
 
@@ -199,7 +230,6 @@
                             int nextstreamid,
                             String key) {
         this.connection = connection;
-        this.client = client2.client();
         this.client2 = client2;
         this.nextstreamid = nextstreamid;
         this.key = key;
@@ -209,102 +239,151 @@
         this.serverSettings = SettingsFrame.getDefaultSettings();
         this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
         this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
-        this.windowUpdater = new ConnectionWindowUpdateSender(this, client.getReceiveBufferSize());
+        debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
+        debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
+        debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
+        this.windowUpdater = new ConnectionWindowUpdateSender(this, client().getReceiveBufferSize());
     }
 
     /**
      * Case 1) Create from upgraded HTTP/1.1 connection.
-     * Is ready to use. Will not be SSL. exchange is the Exchange
+     * Is ready to use. Can be SSL. exchange is the Exchange
      * that initiated the connection, whose response will be delivered
      * on a Stream.
      */
-    Http2Connection(HttpConnection connection,
+    private Http2Connection(HttpConnection connection,
                     Http2ClientImpl client2,
                     Exchange<?> exchange,
-                    ByteBuffer initial)
+                    Supplier<ByteBuffer> initial)
         throws IOException, InterruptedException
     {
         this(connection,
                 client2,
                 3, // stream 1 is registered during the upgrade
                 keyFor(connection));
-        assert !(connection instanceof SSLConnection);
         Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
 
         Stream<?> initialStream = createStream(exchange);
         initialStream.registerStream(1);
         windowController.registerStream(1, getInitialSendWindowSize());
         initialStream.requestSent();
+        // Upgrading:
+        //    set callbacks before sending preface - makes sure anything that
+        //    might be sent by the server will come our way.
+        this.initial = initial;
+        connectFlows(connection);
         sendConnectionPreface();
-        // start reading and writing
-        // start reading
-        AsyncConnection asyncConn = (AsyncConnection)connection;
-        asyncConn.setAsyncCallbacks(this::asyncReceive, this::shutdown, this::getReadBuffer);
-        connection.configureMode(Mode.ASYNC); // set mode only AFTER setAsyncCallbacks to provide visibility.
-        asyncReceive(ByteBufferReference.of(initial));
-        asyncConn.startReading();
     }
 
     // async style but completes immediately
     static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
                                                           Http2ClientImpl client2,
                                                           Exchange<?> exchange,
-                                                          ByteBuffer initial) {
+                                                          Supplier<ByteBuffer> initial)
+    {
         return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
     }
 
+    // Requires TLS handshake. So, is really async
+    static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
+                                                          Http2ClientImpl h2client) {
+        assert request.secure();
+        AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
+        HttpConnection.getConnection(request.getAddress(h2client.client()),
+                                     h2client.client(),
+                                     request,
+                                     HttpClient.Version.HTTP_2);
+
+        return connection.connectAsync()
+                  .thenCompose(unused -> checkSSLConfig(connection))
+                  .thenCompose(notused-> {
+                      CompletableFuture<Http2Connection> cf = new CompletableFuture<>();
+                      try {
+                          Http2Connection hc = new Http2Connection(request, h2client, connection);
+                          cf.complete(hc);
+                      } catch (IOException e) {
+                          cf.completeExceptionally(e);
+                      }
+                      return cf; } );
+    }
+
     /**
      * Cases 2) 3)
      *
      * request is request to be sent.
      */
-    Http2Connection(HttpRequestImpl request, Http2ClientImpl h2client)
-        throws IOException, InterruptedException
+    private Http2Connection(HttpRequestImpl request,
+                            Http2ClientImpl h2client,
+                            HttpConnection connection)
+        throws IOException
     {
-        this(HttpConnection.getConnection(request.getAddress(h2client.client()), h2client.client(), request, true),
-                h2client,
-                1,
-                keyFor(request.uri(), request.proxy(h2client.client())));
+        this(connection,
+             h2client,
+             1,
+             keyFor(request.uri(), request.proxy(h2client.client())));
+
         Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
 
-        // start reading
-        AsyncConnection asyncConn = (AsyncConnection)connection;
-        asyncConn.setAsyncCallbacks(this::asyncReceive, this::shutdown, this::getReadBuffer);
-        connection.connect();
-        checkSSLConfig();
         // safe to resume async reading now.
-        asyncConn.enableCallback();
+        connectFlows(connection);
         sendConnectionPreface();
     }
 
+    private void connectFlows(HttpConnection connection) {
+        FlowTube tube =  connection.getConnectionFlow();
+        // Connect the flow to our Http2TubeSubscriber:
+        // Using connection.publisher() here is a hack that
+        // allows us to continue calling connection.writeAsync()
+        // and connection.flushAsync() transparently.
+        // We will eventually need to implement our own publisher
+        // to write to the flow instead.
+        tube.connectFlows(connection.publisher(), // hack
+                      subscriber);
+    }
+
+    final HttpClientImpl client() {
+        return client2.client();
+    }
+
     /**
      * Throws an IOException if h2 was not negotiated
      */
-    private void checkSSLConfig() throws IOException {
-        AbstractAsyncSSLConnection aconn = (AbstractAsyncSSLConnection)connection;
-        SSLEngine engine = aconn.getEngine();
-        String alpn = engine.getApplicationProtocol();
-        if (alpn == null || !alpn.equals("h2")) {
-            String msg;
-            if (alpn == null) {
-                Log.logSSL("ALPN not supported");
-                msg = "ALPN not supported";
-            } else switch (alpn) {
-              case "":
-                Log.logSSL("No ALPN returned");
-                msg = "No ALPN negotiated";
-                break;
-              case "http/1.1":
-                Log.logSSL("HTTP/1.1 ALPN returned");
-                msg = "HTTP/1.1 ALPN returned";
-                break;
-              default:
-                Log.logSSL("unknown ALPN returned");
-                msg = "Unexpected ALPN: " + alpn;
-                throw new IOException(msg);
+    private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
+        assert aconn.isSecure();
+
+        Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
+            CompletableFuture<Void> cf = new MinimalFuture<>();
+            SSLEngine engine = aconn.getEngine();
+            assert Objects.equals(alpn, engine.getApplicationProtocol());
+
+            DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
+
+            if (alpn == null || !alpn.equals("h2")) {
+                String msg;
+                if (alpn == null) {
+                    Log.logSSL("ALPN not supported");
+                    msg = "ALPN not supported";
+                } else {
+                    switch (alpn) {
+                        case "":
+                            Log.logSSL(msg = "No ALPN negotiated");
+                            break;
+                        case "http/1.1":
+                            Log.logSSL( msg = "HTTP/1.1 ALPN returned");
+                            break;
+                        default:
+                            Log.logSSL(msg = "Unexpected ALPN: " + alpn);
+                            cf.completeExceptionally(new IOException(msg));
+                    }
+                }
+                cf.completeExceptionally(new ALPNException(msg, aconn));
+                return cf;
             }
-            throw new ALPNException(msg, aconn);
-        }
+            cf.complete(null);
+            return cf;
+        };
+
+        return aconn.getALPN().thenCompose(checkAlpnCF);
     }
 
     static String keyFor(HttpConnection connection) {
@@ -322,7 +401,7 @@
         String host;
         int port;
 
-        if (isProxy) {
+        if (proxy != null) {
             host = proxy.getHostString();
             port = proxy.getPort();
         } else {
@@ -381,7 +460,11 @@
         return words.stream().collect(Collectors.joining(" "));
     }
 
-    private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder) {
+    private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
+            throws IOException
+    {
+        debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
+
         boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
 
         ByteBufferReference[] buffers = frame.getHeaderBlock();
@@ -390,7 +473,7 @@
         }
     }
 
-    int getInitialSendWindowSize() {
+    final int getInitialSendWindowSize() {
         return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
     }
 
@@ -400,7 +483,7 @@
         sendFrame(f);
     }
 
-    private ByteBufferPool readBufferPool = new ByteBufferPool();
+    private final ByteBufferPool readBufferPool = new ByteBufferPool();
 
     // provides buffer to read data (default size)
     public ByteBufferReference getReadBuffer() {
@@ -409,7 +492,8 @@
 
     private final Object readlock = new Object();
 
-    public void asyncReceive(ByteBufferReference buffer) {
+    long count;
+    public final void asyncReceive(ByteBufferReference buffer) {
         // We don't need to read anything and
         // we don't want to send anything back to the server
         // until the connection preface has been sent.
@@ -419,11 +503,45 @@
         // SettingsFrame sent by the server) before the connection
         // preface is fully sent might result in the server
         // sending a GOAWAY frame with 'invalid_preface'.
+        //
+        // Note: asyncReceive is only called from the Http2TubeSubscriber
+        //       sequential scheduler. Only asyncReceive uses the readLock.
+        //       Therefore synchronizing on the readlock here should be
+        //       safe.
+        //
         synchronized (readlock) {
             try {
+                Supplier<ByteBuffer> bs = initial;
+                // ensure that we always handle the initial buffer first,
+                // if any.
+                if (bs != null) {
+                    initial = null;
+                    ByteBuffer b = bs.get();
+                    if (b.hasRemaining()) {
+                        long c = ++count;
+                        debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
+                            + c +"): " + b.remaining());
+                        framesController.processReceivedData(framesDecoder,
+                                ByteBufferReference.of(b));
+                    }
+                }
+                ByteBuffer b = buffer.get();
                 // the readlock ensures that the order of incoming buffers
                 // is preserved.
-                framesController.processReceivedData(framesDecoder, buffer);
+                if (b == EMPTY_TRIGGER) {
+                    debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
+                    boolean prefaceSent = framesController.prefaceSent;
+                    assert prefaceSent;
+                    // call framesController.processReceivedData to potentially
+                    // trigger the processing of all the data buffered there.
+                    framesController.processReceivedData(framesDecoder, buffer);
+                    debug.log(Level.DEBUG, "H2 processed buffered data");
+                } else {
+                    long c = ++count;
+                    debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
+                    framesController.processReceivedData(framesDecoder, buffer);
+                    debug.log(Level.DEBUG, "H2 processed(%d)", c);
+                }
             } catch (Throwable e) {
                 String msg = Utils.stackTrace(e);
                 Log.logTrace(msg);
@@ -432,10 +550,20 @@
         }
     }
 
+    Throwable getRecordedCause() {
+        return cause;
+    }
 
     void shutdown(Throwable t) {
+        debug.log(Level.DEBUG, () -> "Shutting down h2c: " + t);
+        if (closed == true) return;
+        synchronized (this) {
+            if (closed == true) return;
+            closed = true;
+        }
         Log.logError(t);
-        closed = true;
+        Throwable initialCause = this.cause;
+        if (initialCause == null) this.cause = t;
         client2.deleteConnection(this);
         List<Stream<?>> c = new LinkedList<>(streams.values());
         for (Stream<?> s : c) {
@@ -457,8 +585,11 @@
         if (frame instanceof MalformedFrame) {
             Log.logError(((MalformedFrame) frame).getMessage());
             if (streamid == 0) {
-                protocolError(((MalformedFrame) frame).getErrorCode());
+                protocolError(((MalformedFrame) frame).getErrorCode(),
+                        ((MalformedFrame) frame).getMessage());
             } else {
+                debug.log(Level.DEBUG, () -> "Reset stream: "
+                          + ((MalformedFrame) frame).getMessage());
                 resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
             }
             return;
@@ -476,6 +607,13 @@
             if (stream == null) {
                 // Should never receive a frame with unknown stream id
 
+                if (frame instanceof HeaderFrame) {
+                    // always decode the headers as they may affect
+                    // connection-level HPACK decoding state
+                    HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+                    decodeHeaders((HeaderFrame) frame, decoder);
+                }
+
                 // To avoid looping, an endpoint MUST NOT send a RST_STREAM in
                 // response to a RST_STREAM frame.
                 if (!(frame instanceof ResetFrame)) {
@@ -499,6 +637,11 @@
     private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
         throws IOException
     {
+        // always decode the headers as they may affect connection-level HPACK
+        // decoding state
+        HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+        decodeHeaders(pp, decoder);
+
         HttpRequestImpl parentReq = parent.request;
         int promisedStreamid = pp.getPromisedStream();
         if (promisedStreamid != nextPushStream) {
@@ -507,8 +650,7 @@
         } else {
             nextPushStream += 2;
         }
-        HeaderDecoder decoder = new HeaderDecoder();
-        decodeHeaders(pp, decoder);
+
         HttpHeadersImpl headers = decoder.headers();
         HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
         Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
@@ -549,7 +691,15 @@
     }
 
     void closeStream(int streamid) {
+        debug.log(Level.DEBUG, "Closed stream %d", streamid);
         Stream<?> s = streams.remove(streamid);
+        if (s != null) {
+            // decrement the reference count on the HttpClientImpl
+            // to allow the SelectorManager thread to exit if no
+            // other operation is pending and the facade is no
+            // longer referenced.
+            client().unreference();
+        }
         // ## Remove s != null. It is a hack for delayed cancellation,reset
         if (s != null && !(s instanceof Stream.PushedStream)) {
             // Since PushStreams have no request body, then they have no
@@ -579,9 +729,15 @@
     private void protocolError(int errorCode)
         throws IOException
     {
+        protocolError(errorCode, null);
+    }
+
+    private void protocolError(int errorCode, String msg)
+        throws IOException
+    {
         GoAwayFrame frame = new GoAwayFrame(0, errorCode);
         sendFrame(frame);
-        shutdown(new IOException("protocol error"));
+        shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
     }
 
     private void handleSettings(SettingsFrame frame)
@@ -655,7 +811,8 @@
         ByteBufferReference ref = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
         Log.logFrames(sf, "OUT");
         // send preface bytes and SettingsFrame together
-        connection.write(ref.get());
+        connection.writeAsync(new ByteBufferReference[] {ref});
+        connection.flushAsync();
         // mark preface sent.
         framesController.markPrefaceSent();
         Log.logTrace("PREFACE_BYTES sent");
@@ -669,6 +826,9 @@
         // cause any pending data stored before the preface was sent to be
         // flushed (see PrefaceController).
         Log.logTrace("finished sending connection preface");
+        debug.log(Level.DEBUG, "Triggering processing of buffered data"
+                  + " after sending connection preface");
+        subscriber.onNext(List.of(EMPTY_TRIGGER));
     }
 
     /**
@@ -682,22 +842,32 @@
     /**
      * Creates Stream with given id.
      */
-    <T> Stream<T> createStream(Exchange<T> exchange) {
-        Stream<T> stream = new Stream<>(client, this, exchange, windowController);
+    final <T> Stream<T> createStream(Exchange<T> exchange) {
+        Stream<T> stream = new Stream<>(client(), this, exchange, windowController);
         return stream;
     }
 
     <T> Stream.PushedStream<?,T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
         PushGroup<?,T> pg = parent.exchange.getPushGroup();
-        return new Stream.PushedStream<>(pg, client, this, parent, pushEx);
+        return new Stream.PushedStream<>(pg, client(), this, parent, pushEx);
     }
 
     <T> void putStream(Stream<T> stream, int streamid) {
+        // increment the reference count on the HttpClientImpl
+        // to prevent the SelectorManager thread from exiting until
+        // the stream is closed.
+        client().reference();
         streams.put(streamid, stream);
     }
 
     void deleteStream(int streamid) {
-        streams.remove(streamid);
+        if (streams.remove(streamid) != null) {
+            // decrement the reference count on the HttpClientImpl
+            // to allow the SelectorManager thread to exit if no
+            // other operation is pending and the facade is no
+            // longer referenced.
+            client().unreference();
+        }
         windowController.removeStream(streamid);
     }
 
@@ -728,7 +898,7 @@
     // There can be no concurrent access to this  buffer as all access to this buffer
     // and its content happen within a single critical code block section protected
     // by the sendLock. / (see sendFrame())
-    private ByteBufferPool headerEncodingPool = new ByteBufferPool();
+    private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
 
     private ByteBufferReference getHeaderBuffer(int maxFrameSize) {
         ByteBufferReference ref = headerEncodingPool.get(maxFrameSize);
@@ -872,6 +1042,208 @@
         }
     }
 
+    /**
+     * Returns the TubeSubscriber for reading from the connection flow.
+     * @return the TubeSubscriber for reading from the connection flow.
+     */
+    TubeSubscriber subscriber() {
+        return subscriber;
+    }
+
+    /**
+     * A simple tube subscriber for reading from the connection flow.
+     */
+    final class Http2TubeSubscriber implements TubeSubscriber {
+        volatile Flow.Subscription subscription;
+        volatile boolean completed;
+        volatile boolean dropped;
+        volatile Throwable error;
+        final ConcurrentLinkedQueue<ByteBuffer> queue
+                = new ConcurrentLinkedQueue<>();
+        final SequentialScheduler scheduler = new SequentialScheduler(
+                        new SynchronizedRestartableTask(this::processQueue));
+
+        final void processQueue() {
+            try {
+                while (!queue.isEmpty() && !scheduler.isStopped()) {
+                    ByteBuffer buffer = queue.poll();
+                    debug.log(Level.DEBUG,
+                              "sending %d to Http2Connection.asyncReceive",
+                              buffer.remaining());
+                    asyncReceive(ByteBufferReference.of(buffer));
+                }
+            } catch (Throwable t) {
+                Throwable x = error;
+                if (x == null) error = t;
+            } finally {
+                Throwable x = error;
+                if (x != null) {
+                    debug.log(Level.DEBUG, "Stopping scheduler", x);
+                    scheduler.stop();
+                    Http2Connection.this.shutdown(x);
+                }
+            }
+        }
+
+
+        public void onSubscribe(Flow.Subscription subscription) {
+            // supports being called multiple time.
+            // doesn't cancel the previous subscription, since that is
+            // most probably the same as the new subscription.
+            assert this.subscription == null || dropped == false;
+            this.subscription = subscription;
+            dropped = false;
+            // TODO FIXME: request(1) should be done by the delegate.
+            if (!completed) {
+                debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
+                subscription.request(Long.MAX_VALUE);
+            } else {
+                debug.log(Level.DEBUG, "onSubscribe: already completed");
+            }
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
+                    + " bytes in " + item.size() + " buffers");
+            queue.addAll(item);
+            scheduler.deferOrSchedule(client().theExecutor());
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            debug.log(Level.DEBUG, () -> "onError: " + throwable);
+            error = throwable;
+            completed = true;
+            scheduler.deferOrSchedule(client().theExecutor());
+        }
+
+        @Override
+        public void onComplete() {
+            debug.log(Level.DEBUG, "EOF");
+            error = new EOFException("EOF reached while reading");
+            completed = true;
+            scheduler.deferOrSchedule(client().theExecutor());
+        }
+
+        public void dropSubscription() {
+            debug.log(Level.DEBUG, "dropSubscription");
+            // we could probably set subscription to null here...
+            // then we might not need the 'dropped' boolean?
+            dropped = true;
+        }
+    }
+
+    @Override
+    public final String toString() {
+        return dbgString();
+    }
+
+    final String dbgString() {
+        return "Http2Connection("
+                    + connection.getConnectionFlow() + ")";
+    }
+
+    final class LoggingHeaderDecoder extends HeaderDecoder {
+
+        private final HeaderDecoder delegate;
+        private final System.Logger debugHpack =
+                Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+
+        LoggingHeaderDecoder(HeaderDecoder delegate) {
+            this.delegate = delegate;
+        }
+
+        String dbgString() {
+            return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
+        }
+
+        @Override
+        public void onDecoded(CharSequence name, CharSequence value) {
+            delegate.onDecoded(name, value);
+        }
+
+        @Override
+        public void onIndexed(int index,
+                              CharSequence name,
+                              CharSequence value) {
+            debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
+                           index, name, value);
+            delegate.onIndexed(index, name, value);
+        }
+
+        @Override
+        public void onLiteral(int index,
+                              CharSequence name,
+                              CharSequence value,
+                              boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+                              index, name, value, valueHuffman);
+            delegate.onLiteral(index, name, value, valueHuffman);
+        }
+
+        @Override
+        public void onLiteral(CharSequence name,
+                              boolean nameHuffman,
+                              CharSequence value,
+                              boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+                           name, nameHuffman, value, valueHuffman);
+            delegate.onLiteral(name, nameHuffman, value, valueHuffman);
+        }
+
+        @Override
+        public void onLiteralNeverIndexed(int index,
+                                          CharSequence name,
+                                          CharSequence value,
+                                          boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+                           index, name, value, valueHuffman);
+            delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
+        }
+
+        @Override
+        public void onLiteralNeverIndexed(CharSequence name,
+                                          boolean nameHuffman,
+                                          CharSequence value,
+                                          boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+                           name, nameHuffman, value, valueHuffman);
+            delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
+        }
+
+        @Override
+        public void onLiteralWithIndexing(int index,
+                                          CharSequence name,
+                                          CharSequence value,
+                                          boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+                           index, name, value, valueHuffman);
+            delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
+        }
+
+        @Override
+        public void onLiteralWithIndexing(CharSequence name,
+                                          boolean nameHuffman,
+                                          CharSequence value,
+                                          boolean valueHuffman) {
+            debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+                              name, nameHuffman, value, valueHuffman);
+            delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
+        }
+
+        @Override
+        public void onSizeUpdate(int capacity) {
+            debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
+            delegate.onSizeUpdate(capacity);
+        }
+
+        @Override
+        HttpHeadersImpl headers() {
+            return delegate.headers();
+        }
+    }
+
     static class HeaderDecoder implements DecodingCallback {
         HttpHeadersImpl headers;
 
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -34,6 +34,8 @@
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLParameters;
 
@@ -103,28 +105,31 @@
         public abstract Builder cookieManager(CookieManager cookieManager);
 
         /**
-         * Sets an {@code SSLContext}. If a security manager is set, then the caller
-         * must have the {@link java.net.NetPermission NetPermission}
-         * ({@code "setSSLContext"})
+         * Sets an {@code SSLContext}.
          *
-         * <p> The effect of not calling this method, is that a default {@link
-         * javax.net.ssl.SSLContext} is used, which is normally adequate for
-         * client applications that do not need to specify protocols, or require
-         * client authentication.
+         * <p> If this method is not invoked before {@linkplain #build() build},
+         * then the {@link SSLContext#getDefault() default context} is used,
+         * which is normally adequate for client applications that do not need
+         * to specify protocols, or require client authentication.
          *
          * @param sslContext the SSLContext
          * @return this builder
-         * @throws SecurityException if a security manager is set and the
-         *                           caller does not have any required permission
+         * @throws SecurityException if a security manager has been installed
+         *          and it denies {@linkplain java.net.NetPermission}
+         *          ({@code "setSSLContext"})
          */
         public abstract Builder sslContext(SSLContext sslContext);
 
         /**
-         * Sets an {@code SSLParameters}. If this method is not called, then a default
-         * set of parameters are used. The contents of the given object are
-         * copied. Some parameters which are used internally by the HTTP protocol
-         * implementation (such as application protocol list) should not be set
-         * by callers, as they are ignored.
+         * Sets an {@code SSLParameters}.
+         *
+         * <p> If this method is not invoked before {@linkplain #build() build},
+         * then a default, implementation specific, set of parameters are used.
+         *
+         * <p> Some parameters which are used internally by the HTTP Client
+         * implementation (such as the application protocol list) should not be
+         * set by callers, as they may be ignored. The contents of the given
+         * object are copied.
          *
          * @param sslParameters the SSLParameters
          * @return this builder
@@ -132,10 +137,17 @@
         public abstract Builder sslParameters(SSLParameters sslParameters);
 
         /**
-         * Sets the executor to be used for asynchronous tasks. If this method is
-         * not called, a default executor is set, which is the one returned from {@link
-         * java.util.concurrent.Executors#newCachedThreadPool()
-         * Executors.newCachedThreadPool}.
+         * Sets the executor to be used for asynchronous and dependent tasks.
+         *
+         * <p> If this method is not invoked before {@linkplain #build() build},
+         * a default executor is created for each newly built {@code HttpClient}.
+         * The default executor uses a {@linkplain
+         * Executors#newCachedThreadPool(ThreadFactory) cached thread pool}, with
+         * a custom thread factory.
+         *
+         * @implNote If a security manager has been installed, the thread
+         * factory creates threads that run with an access control context that
+         * has no permissions.
          *
          * @param executor the Executor
          * @return this builder
@@ -144,8 +156,9 @@
 
         /**
          * Specifies whether requests will automatically follow redirects issued
-         * by the server. This setting can be overridden on each request. The
-         * default value for this setting is {@link Redirect#NEVER NEVER}
+         * by the server. The default redirection policy for clients built by
+         * this builder, if this method has not been invoked, is {@link
+         * Redirect#NEVER NEVER}.
          *
          * @param policy the redirection policy
          * @return this builder
@@ -180,12 +193,12 @@
         public abstract Builder priority(int priority);
 
         /**
-         * Sets a {@link java.net.ProxySelector} for this client. If no selector
-         * is set, then no proxies are used. If a {@code null} parameter is
-         * given then the system wide default proxy selector is used.
+         * Sets a {@link java.net.ProxySelector}.
          *
-         * @implNote {@link java.net.ProxySelector#of(InetSocketAddress)}
-         * provides a {@code ProxySelector} which uses one proxy for all requests.
+         * @implNote {@link ProxySelector#of(InetSocketAddress)}
+         * provides a {@code ProxySelector} which uses a single proxy for all
+         * requests. The system-wide proxy selector can be retrieved by
+         * {@link ProxySelector#getDefault()}.
          *
          * @param selector the ProxySelector
          * @return this builder
@@ -211,50 +224,55 @@
 
 
     /**
-     * Returns an {@code Optional} which contains this client's {@link
-     * CookieManager}. If no {@code CookieManager} was set in this client's builder,
-     * then the {@code Optional} is empty.
+     * Returns an {@code Optional} containing this client's {@link
+     * CookieManager}. If no {@code CookieManager} was set in this client's
+     * builder, then the {@code Optional} is empty.
      *
      * @return an {@code Optional} containing this client's {@code CookieManager}
      */
     public abstract Optional<CookieManager> cookieManager();
 
     /**
-     * Returns the follow-redirects setting for this client. The default value
-     * for this setting is {@link HttpClient.Redirect#NEVER}
+     * 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}.
      *
      * @return this client's follow redirects setting
      */
     public abstract Redirect followRedirects();
 
     /**
-     * Returns an {@code Optional} containing the {@code ProxySelector} for this client.
-     * If no proxy is set then the {@code Optional} is empty.
+     * Returns an {@code Optional} containing this client's {@code ProxySelector}.
+     * If no proxy selector was set in this client's builder, then the {@code
+     * Optional} is empty.
      *
      * @return an {@code Optional} containing this client's proxy selector
      */
     public abstract Optional<ProxySelector> proxy();
 
     /**
-     * Returns the {@code SSLContext}, if one was set on this client. If a security
-     * manager is set, then the caller must have the
-     * {@link java.net.NetPermission NetPermission}("getSSLContext") permission.
-     * If no {@code SSLContext} was set, then the default context is returned.
+     * Returns this client's {@code SSLContext}.
+     *
+     * <p> If no {@code SSLContext} was set in this client's builder, then the
+     * {@linkplain SSLContext#getDefault() default context} is returned.
      *
      * @return this client's SSLContext
-     * @throws SecurityException if the caller does not have permission to get
-     *         the SSLContext
+     * @throws SecurityException if a security manager has been installed
+     *          and it denies {@linkplain java.net.NetPermission}
+     *          ({@code "getSSLContext"})
      */
     public abstract SSLContext sslContext();
 
     /**
-     * Returns an {@code Optional} containing the {@link SSLParameters} set on
-     * this client. If no {@code SSLParameters} were set in the client's builder,
-     * then the {@code Optional} is empty.
+     * Returns a copy of this client's {@link SSLParameters}.
      *
-     * @return an {@code Optional} containing this client's {@code SSLParameters}
+     * <p> If no {@code SSLParameters} were set in the client's builder, then an
+     * implementation specific default set of parameters, that the client will
+     * use, is returned.
+     *
+     * @return this client's {@code SSLParameters}
      */
-    public abstract Optional<SSLParameters> sslParameters();
+    public abstract SSLParameters sslParameters();
 
     /**
      * Returns an {@code Optional} containing the {@link Authenticator} set on
@@ -274,14 +292,18 @@
     public abstract HttpClient.Version version();
 
     /**
-     * Returns the {@code Executor} set on this client. If an {@code
-     * Executor} was not set on the client's builder, then a default
-     * object is returned. The default {@code Executor} is created independently
-     * for each client.
+     * Returns an {@code Optional} containing this client's {@linkplain
+     * Executor}. If no {@code Executor} was set in the client's builder,
+     * then the {@code Optional} is empty.
      *
-     * @return this client's Executor
+     * <p> Even though this method may return an empty optional, the {@code
+     * HttpClient} may still have an non-exposed {@linkplain
+     * HttpClient.Builder#executor(Executor) default executor} that is used for
+     * executing asynchronous and dependent tasks.
+     *
+     * @return an {@code Optional} containing this client's {@code Executor}
      */
-    public abstract Executor executor();
+    public abstract Optional<Executor> executor();
 
     /**
      * The HTTP protocol version.
@@ -351,6 +373,11 @@
      * @return the response body
      * @throws java.io.IOException if an I/O error occurs when sending or receiving
      * @throws java.lang.InterruptedException if the operation is interrupted
+     * @throws SecurityException If a security manager has been installed
+     *          and it denies {@link java.net.URLPermission access} to the
+     *          URL in the given request, or proxy if one is configured.
+     *          See HttpRequest for further information about
+     *          <a href="HttpRequest.html#securitychecks">security checks</a>.
      */
     public abstract <T> HttpResponse<T>
     send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
@@ -360,6 +387,12 @@
      * Sends the given request asynchronously using this client and the given
      * response handler.
      *
+     * <p> The returned completable future is completed with a SecurityException
+     * if a security manager has been installed and it denies {@link
+     * java.net.URLPermission access} to the URI in the given request, or proxy
+     * if one is configured. See HttpRequest for further information about
+     * <a href="HttpRequest.html#securitychecks">security checks</a>.
+     *
      * @param <T> the response body type
      * @param req the request
      * @param responseBodyHandler the response body handler
@@ -372,14 +405,20 @@
      * Sends the given request asynchronously using this client and the given
      * multi response handler.
      *
+     * <p> The returned completable future is completed with a SecurityException
+     * if a security manager has been installed and it denies {@link
+     * java.net.URLPermission access} to the URI in the given request, or proxy
+     * if one is configured. See HttpRequest for further information about
+     * <a href="HttpRequest.html#securitychecks">security checks</a>.
+     *
      * @param <U> a type representing the aggregated results
      * @param <T> a type representing all of the response bodies
      * @param req the request
-     * @param multiProcessor the MultiProcessor for the request
+     * @param multiSubscriber the multiSubscriber for the request
      * @return a {@code CompletableFuture<U>}
      */
     public abstract <U, T> CompletableFuture<U>
-    sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U, T> multiProcessor);
+    sendAsync(HttpRequest req, HttpResponse.MultiSubscriber<U, T> multiSubscriber);
 
     /**
      * Creates a builder of {@link WebSocket} instances connected to the given
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -27,6 +27,7 @@
 
 import java.net.Authenticator;
 import java.net.CookieManager;
+import java.net.NetPermission;
 import java.net.ProxySelector;
 import java.util.concurrent.Executor;
 import javax.net.ssl.SSLContext;
@@ -58,7 +59,11 @@
     @Override
     public HttpClientBuilderImpl sslContext(SSLContext sslContext) {
         requireNonNull(sslContext);
-        Utils.checkNetPermission("setSSLContext");
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            NetPermission np = new NetPermission("setSSLContext");
+            sm.checkPermission(np);
+        }
         this.sslContext = sslContext;
         return this;
     }
@@ -67,7 +72,7 @@
     @Override
     public HttpClientBuilderImpl sslParameters(SSLParameters sslParameters) {
         requireNonNull(sslParameters);
-        this.sslParams = sslParameters;
+        this.sslParams = Utils.copySSLParameters(sslParameters);
         return this;
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientFacade.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+/**
+ * An HttpClientFacade is a simple class that wraps an HttpClient implementation
+ * and delegates everything to its implementation delegate.
+ */
+final class HttpClientFacade extends HttpClient {
+
+    final HttpClientImpl impl;
+
+    /**
+     * Creates an HttpClientFacade.
+     */
+    HttpClientFacade(HttpClientImpl impl) {
+        this.impl = impl;
+    }
+
+    @Override
+    public Optional<CookieManager> cookieManager() {
+        return impl.cookieManager();
+    }
+
+    @Override
+    public Redirect followRedirects() {
+        return impl.followRedirects();
+    }
+
+    @Override
+    public Optional<ProxySelector> proxy() {
+        return impl.proxy();
+    }
+
+    @Override
+    public SSLContext sslContext() {
+        return impl.sslContext();
+    }
+
+    @Override
+    public SSLParameters sslParameters() {
+        return impl.sslParameters();
+    }
+
+    @Override
+    public Optional<Authenticator> authenticator() {
+        return impl.authenticator();
+    }
+
+    @Override
+    public HttpClient.Version version() {
+        return impl.version();
+    }
+
+    @Override
+    public Optional<Executor> executor() {
+        return impl.executor();
+    }
+
+    @Override
+    public <T> HttpResponse<T>
+    send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
+        throws IOException, InterruptedException
+    {
+        try {
+            return impl.send(req, responseBodyHandler);
+        } finally {
+            Reference.reachabilityFence(this);
+        }
+    }
+
+    @Override
+    public <T> CompletableFuture<HttpResponse<T>>
+    sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler) {
+        try {
+            return impl.sendAsync(req, responseBodyHandler);
+        } finally {
+            Reference.reachabilityFence(this);
+        }
+    }
+
+    @Override
+    public <U, T> CompletableFuture<U>
+    sendAsync(HttpRequest req, HttpResponse.MultiSubscriber<U, T> multiSubscriber) {
+        try {
+            return impl.sendAsync(req, multiSubscriber);
+        } finally {
+            Reference.reachabilityFence(this);
+        }
+    }
+
+    @Override
+    public WebSocket.Builder newWebSocketBuilder(URI uri,
+                                                 WebSocket.Listener listener)
+    {
+        try {
+            return impl.newWebSocketBuilder(uri, listener);
+        } finally {
+            Reference.reachabilityFence(this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        // Used by tests to get the client's id.
+        return impl.toString();
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -28,33 +28,47 @@
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLParameters;
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.lang.ref.WeakReference;
 import java.net.Authenticator;
 import java.net.CookieManager;
+import java.net.NetPermission;
 import java.net.ProxySelector;
 import java.net.URI;
+import java.nio.channels.CancelledKeyException;
 import java.nio.channels.ClosedChannelException;
 import java.nio.channels.SelectableChannel;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.SocketChannel;
+import java.security.AccessControlContext;
+import java.security.AccessController;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivilegedAction;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Stream;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.MultiSubscriber;
 import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Pair;
 import jdk.incubator.http.internal.common.Utils;
 import jdk.incubator.http.internal.websocket.BuilderImpl;
+import jdk.internal.misc.InnocuousThread;
 
 /**
  * Client implementation. Contains all configuration information and also
@@ -63,20 +77,39 @@
  */
 class HttpClientImpl extends HttpClient {
 
+    static final boolean DEBUG = Utils.DEBUG;  // Revisit: temporary dev flag.
+    static final boolean DEBUGELAPSED = Utils.ASSERTIONSENABLED || DEBUG;  // Revisit: temporary dev flag.
+    static final boolean DEBUGTIMEOUT = false; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+    final System.Logger  debugelapsed = Utils.getDebugLogger(this::dbgString, DEBUGELAPSED);
+    final System.Logger  debugtimeout = Utils.getDebugLogger(this::dbgString, DEBUGTIMEOUT);
+    static final AtomicLong CLIENT_IDS = new AtomicLong();
+
     // Define the default factory as a static inner class
     // that embeds all the necessary logic to avoid
     // the risk of using a lambda that might keep a reference on the
     // HttpClient instance from which it was created (helps with
     // heapdump analysis).
     private static final class DefaultThreadFactory implements ThreadFactory {
-        private DefaultThreadFactory() {}
+        private final String namePrefix;
+        private final AtomicInteger nextId = new AtomicInteger();
+
+        DefaultThreadFactory(long clientID) {
+            namePrefix = "HttpClient-" + clientID + "-Worker-";
+        }
+
         @Override
         public Thread newThread(Runnable r) {
-            Thread t = new Thread(null, r, "HttpClient_worker", 0, true);
+            String name = namePrefix + nextId.getAndIncrement();
+            Thread t;
+            if (System.getSecurityManager() == null) {
+                t = new Thread(null, r, name, 0, false);
+            } else {
+                t = InnocuousThread.newThread(name, r);
+            }
             t.setDaemon(true);
             return t;
         }
-        static final ThreadFactory INSTANCE = new DefaultThreadFactory();
     }
 
     private final CookieManager cookieManager;
@@ -86,23 +119,95 @@
     private final Version version;
     private final ConnectionPool connections;
     private final Executor executor;
+    private final boolean isDefaultExecutor;
     // Security parameters
     private final SSLContext sslContext;
     private final SSLParameters sslParams;
     private final SelectorManager selmgr;
     private final FilterFactory filters;
     private final Http2ClientImpl client2;
+    private final long id;
+    private final String dbgTag;
+
+    // This reference is used to keep track of the facade HttpClient
+    // that was returned to the application code.
+    // It makes it possible to know when the application no longer
+    // holds any reference to the HttpClient.
+    // Unfortunately, this information is not enough to know when
+    // to exit the SelectorManager thread. Because of the asynchronous
+    // nature of the API, we also need to wait until all pending operations
+    // have completed.
+    private final WeakReference<HttpClientFacade> facadeRef;
+
+    // This counter keeps track of the number of operations pending
+    // on the HttpClient. The SelectorManager thread will wait
+    // until there are no longer any pending operations and the
+    // facadeRef is cleared before exiting.
+    //
+    // The pendingOperationCount is incremented every time a send/sendAsync
+    // operation is invoked on the HttpClient, and is decremented when
+    // the HttpResponse<T> object is returned to the user.
+    // However, at this point, the body may not have been fully read yet.
+    // This is the case when the response T is implemented as a streaming
+    // subscriber (such as an InputStream).
+    //
+    // To take care of this issue the pendingOperationCount will additionally
+    // be incremented/decremented in the following cases:
+    //
+    // 1. For HTTP/2  it is incremented when a stream is added to the
+    //    Http2Connection streams map, and decreased when the stream is removed
+    //    from the map. This should also take care of push promises.
+    // 2. For WebSocket the count is increased when creating a
+    //    DetachedConnectionChannel for the socket, and decreased
+    //    when the the channel is closed.
+    //    In addition, the HttpClient facade is passed to the WebSocket builder,
+    //    (instead of the client implementation delegate).
+    // 3. For HTTP/1.1 the count is incremented before starting to parse the body
+    //    response, and decremented when the parser has reached the end of the
+    //    response body flow.
+    //
+    // This should ensure that the selector manager thread remains alive until
+    // the response has been fully received or the web socket is closed.
+    private final AtomicLong pendingOperationCount = new AtomicLong();
+    private final AtomicLong pendingWebSocketCount = new AtomicLong();
+    private final AtomicLong pendingHttpRequestCount = new AtomicLong();
 
     /** A Set of, deadline first, ordered timeout events. */
     private final TreeSet<TimeoutEvent> timeouts;
 
-    public static HttpClientImpl create(HttpClientBuilderImpl builder) {
-        HttpClientImpl impl = new HttpClientImpl(builder);
-        impl.start();
-        return impl;
+    /**
+     * This is a bit tricky:
+     * 1. an HttpClientFacade has a final HttpClientImpl field.
+     * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
+     *    where the referent is the facade created for that instance.
+     * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
+     *    constructor, because it would be only weakly referenced and could
+     *    be GC'ed before we can return it.
+     * The solution is to use an instance of SingleFacadeFactory which will
+     * allow the caller of new HttpClientImpl(...) to retrieve the facade
+     * after the HttpClientImpl has been created.
+     */
+    private static final class SingleFacadeFactory {
+        HttpClientFacade facade;
+        HttpClientFacade createFacade(HttpClientImpl impl) {
+            assert facade == null;
+            return (facade = new HttpClientFacade(impl));
+        }
     }
 
-    private HttpClientImpl(HttpClientBuilderImpl builder) {
+    static HttpClientFacade create(HttpClientBuilderImpl builder) {
+        SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
+        HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
+        impl.start();
+        assert facadeFactory.facade != null;
+        assert impl.facadeRef.get() == facadeFactory.facade;
+        return facadeFactory.facade;
+    }
+
+    private HttpClientImpl(HttpClientBuilderImpl builder,
+                           SingleFacadeFactory facadeFactory) {
+        id = CLIENT_IDS.incrementAndGet();
+        dbgTag = "HttpClientImpl(" + id +")";
         if (builder.sslContext == null) {
             try {
                 sslContext = SSLContext.getDefault();
@@ -114,10 +219,13 @@
         }
         Executor ex = builder.executor;
         if (ex == null) {
-            ex = Executors.newCachedThreadPool(DefaultThreadFactory.INSTANCE);
+            ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
+            isDefaultExecutor = true;
         } else {
             ex = builder.executor;
+            isDefaultExecutor = false;
         }
+        facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
         client2 = new Http2ClientImpl(this);
         executor = ex;
         cookieManager = builder.cookieManager;
@@ -135,7 +243,7 @@
         } else {
             sslParams = builder.sslParams;
         }
-        connections = new ConnectionPool();
+        connections = new ConnectionPool(id);
         connections.start();
         timeouts = new TreeSet<>();
         try {
@@ -147,30 +255,97 @@
         selmgr.setDaemon(true);
         filters = new FilterFactory();
         initFilters();
+        assert facadeRef.get() != null;
     }
 
     private void start() {
         selmgr.start();
     }
 
+    // Called from the SelectorManager thread, just before exiting.
+    // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
+    // that may be still lingering there are properly closed (and their
+    // possibly still opened SocketChannel released).
+    private void stop() {
+        // Clears HTTP/1.1 cache and close its connections
+        connections.stop();
+        // Clears HTTP/2 cache and close its connections.
+        client2.stop();
+    }
+
     private static SSLParameters getDefaultParams(SSLContext ctx) {
         SSLParameters params = ctx.getSupportedSSLParameters();
         params.setProtocols(new String[]{"TLSv1.2"});
         return params;
     }
 
+    // Returns the facade that was returned to the application code.
+    // May be null if that facade is no longer referenced.
+    final HttpClientFacade facade() {
+        return facadeRef.get();
+    }
+
+    // Increments the pendingOperationCount.
+    final long reference() {
+        pendingHttpRequestCount.incrementAndGet();
+        return pendingOperationCount.incrementAndGet();
+    }
+
+    // Decrements the pendingOperationCount.
+    final long unreference() {
+        final long count = pendingOperationCount.decrementAndGet();
+        final long httpCount = pendingHttpRequestCount.decrementAndGet();
+        final long webSocketCount = pendingWebSocketCount.get();
+        if (count == 0 && facade() == null) {
+            selmgr.wakeupSelector();
+        }
+        assert httpCount >= 0 : "count of HTTP operations < 0";
+        assert webSocketCount >= 0 : "count of WS operations < 0";
+        assert count >= 0 : "count of pending operations < 0";
+        return count;
+    }
+
+    // Increments the pendingOperationCount.
+    final long webSocketOpen() {
+        pendingWebSocketCount.incrementAndGet();
+        return pendingOperationCount.incrementAndGet();
+    }
+
+    // Decrements the pendingOperationCount.
+    final long webSocketClose() {
+        final long count = pendingOperationCount.decrementAndGet();
+        final long webSocketCount = pendingWebSocketCount.decrementAndGet();
+        final long httpCount = pendingHttpRequestCount.get();
+        if (count == 0 && facade() == null) {
+            selmgr.wakeupSelector();
+        }
+        assert httpCount >= 0 : "count of HTTP operations < 0";
+        assert webSocketCount >= 0 : "count of WS operations < 0";
+        assert count >= 0 : "count of pending operations < 0";
+        return count;
+    }
+
+    // Returns the pendingOperationCount.
+    final long referenceCount() {
+        return pendingOperationCount.get();
+    }
+
+    // Called by the SelectorManager thread to figure out whether it's time
+    // to terminate.
+    final boolean isReferenced() {
+        HttpClient facade = facade();
+        return facade != null || referenceCount() > 0;
+    }
+
     /**
-     * Wait for activity on given exchange (assuming blocking = false).
-     * It's a no-op if blocking = true. In particular, the following occurs
-     * in the SelectorManager thread.
+     * Wait for activity on given exchange.
+     * The following occurs in the SelectorManager thread.
      *
-     *  1) mark the connection non-blocking
-     *  2) add to selector
-     *  3) If selector fires for this exchange then
-     *  4)   - mark connection as blocking
-     *  5)   - call AsyncEvent.handle()
+     *  1) add to selector
+     *  2) If selector fires for this exchange then
+     *     call AsyncEvent.handle()
      *
-     * If exchange needs to block again, then call registerEvent() again
+     * If exchange needs to change interest ops, then call registerEvent() again.
      */
     void registerEvent(AsyncEvent exchange) throws IOException {
         selmgr.register(exchange);
@@ -184,131 +359,178 @@
         selmgr.cancel(s);
     }
 
+    /**
+     * Allows an AsyncEvent to modify its interestOps.
+     * @param event The modified event.
+     */
+    void eventUpdated(AsyncEvent event) throws ClosedChannelException {
+        assert !(event instanceof AsyncTriggerEvent);
+        selmgr.eventUpdated(event);
+    }
+
+    boolean isSelectorThread() {
+        return Thread.currentThread() == selmgr;
+    }
 
     Http2ClientImpl client2() {
         return client2;
     }
 
-    /*
-    @Override
-    public ByteBuffer getBuffer() {
-        return pool.getBuffer();
-    }
-
-    // SSL buffers are larger. Manage separately
-
-    int size = 16 * 1024;
-
-    ByteBuffer getSSLBuffer() {
-        return ByteBuffer.allocate(size);
-    }
-
-    /**
-     * Return a new buffer that's a bit bigger than the given one
-     *
-     * @param buf
-     * @return
-     *
-    ByteBuffer reallocSSLBuffer(ByteBuffer buf) {
-        size = buf.capacity() * 12 / 10; // 20% bigger
-        return ByteBuffer.allocate(size);
-    }
-
-    synchronized void returnSSLBuffer(ByteBuffer buf) {
-        if (buf.capacity() >= size)
-           sslBuffers.add(0, buf);
+    private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+        if (debugelapsed.isLoggable(Level.DEBUG)) {
+            debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
+                    + (System.nanoTime() - startNanos)/1000_000L
+                    + " millis for " + req.method()
+                    + " to " + req.uri());
+        }
     }
 
     @Override
-    public void returnBuffer(ByteBuffer buffer) {
-        pool.returnBuffer(buffer);
-    }
-    */
-
-    @Override
     public <T> HttpResponse<T>
-    send(HttpRequest req, HttpResponse.BodyHandler<T> responseHandler)
+    send(HttpRequest req, BodyHandler<T> responseHandler)
         throws IOException, InterruptedException
     {
-        MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler);
-        return mex.response();
+        try {
+            return sendAsync(req, responseHandler).join();
+        } catch (CompletionException ce) {
+            Throwable t = ce.getCause();
+            if (t instanceof Error)
+                throw (Error)t;
+            if (t instanceof RuntimeException)
+                throw (RuntimeException)t;
+            else if (t instanceof IOException)
+                throw Utils.getIOException(t);
+            else
+                throw new InternalError("Unexpected exception", t);
+        }
     }
 
     @Override
     public <T> CompletableFuture<HttpResponse<T>>
-    sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseHandler)
+    sendAsync(HttpRequest req, BodyHandler<T> responseHandler)
     {
-        MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler);
-        return mex.responseAsync()
-                  .thenApply((HttpResponseImpl<T> b) -> (HttpResponse<T>) b);
+        AccessControlContext acc = null;
+        if (System.getSecurityManager() != null)
+            acc = AccessController.getContext();
+
+        long start = DEBUGELAPSED ? System.nanoTime() : 0;
+        reference();
+        try {
+            debug.log(Level.DEBUG, "ClientImpl (async) send %s", req);
+
+            MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+            CompletableFuture<HttpResponse<T>> res =
+                    mex.responseAsync().whenComplete((b,t) -> unreference());
+            if (DEBUGELAPSED) {
+                res = res.whenComplete(
+                        (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+            }
+            // makes sure that any dependent actions happen in the executor
+            if (acc != null) {
+                res.whenCompleteAsync((r, t) -> { /* do nothing */},
+                                      new PrivilegedExecutor(executor, acc));
+            }
+
+            return res;
+        } catch(Throwable t) {
+            unreference();
+            debugCompleted("ClientImpl (async)", start, req);
+            throw t;
+        }
     }
 
     @Override
     public <U, T> CompletableFuture<U>
-    sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U, T> responseHandler) {
-        MultiExchange<U,T> mex = new MultiExchange<>(req, this, responseHandler);
-        return mex.multiResponseAsync();
-    }
+    sendAsync(HttpRequest req, MultiSubscriber<U, T> responseHandler) {
+        AccessControlContext acc = null;
+        if (System.getSecurityManager() != null)
+            acc = AccessController.getContext();
 
-    // new impl. Should get rid of above
-    /*
-    static class BufferPool implements BufferHandler {
-
-        final LinkedList<ByteBuffer> freelist = new LinkedList<>();
+        long start = DEBUGELAPSED ? System.nanoTime() : 0;
+        reference();
+        try {
+            debug.log(Level.DEBUG, "ClientImpl (async) send multi %s", req);
 
-        @Override
-        public synchronized ByteBuffer getBuffer() {
-            ByteBuffer buf;
+            MultiExchange<U,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+            CompletableFuture<U> res = mex.multiResponseAsync()
+                      .whenComplete((b,t) -> unreference());
+            if (DEBUGELAPSED) {
+                res = res.whenComplete(
+                        (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+            }
+            // makes sure that any dependent actions happen in the executor
+            if (acc != null) {
+                res.whenCompleteAsync((r, t) -> { /* do nothing */},
+                                      new PrivilegedExecutor(executor, acc));
+            }
 
-            while (!freelist.isEmpty()) {
-                buf = freelist.removeFirst();
-                buf.clear();
-                return buf;
-            }
-            return ByteBuffer.allocate(BUFSIZE);
-        }
-
-        @Override
-        public synchronized void returnBuffer(ByteBuffer buffer) {
-            assert buffer.capacity() > 0;
-            freelist.add(buffer);
+            return res;
+        } catch(Throwable t) {
+            unreference();
+            debugCompleted("ClientImpl (async)", start, req);
+            throw t;
         }
     }
 
-    static BufferPool pool = new BufferPool();
-
-    static BufferHandler pool() {
-        return pool;
-    }
-*/
     // Main loop for this client's selector
     private final static class SelectorManager extends Thread {
 
-        private static final long NODEADLINE = 3000L;
+        // For testing purposes we have an internal System property that
+        // can control the frequency at which the selector manager will wake
+        // up when there are no pending operations.
+        // Increasing the frequency (shorter delays) might allow the selector
+        // to observe that the facade is no longer referenced and might allow
+        // the selector thread to terminate more timely - for when nothing is
+        // ongoing it will only check for that condition every NODEADLINE ms.
+        // To avoid misuse of the property, the delay that can be specified
+        // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
+        // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
+        // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
+        private static final int MIN_NODEADLINE = 1000; // ms
+        private static final int MAX_NODEADLINE = 1000 * 1200; // ms
+        private static final int DEF_NODEADLINE = 3000; // ms
+        private static final long NODEADLINE; // default is DEF_NODEADLINE ms
+        static {
+            // ensure NODEADLINE is inialized with some valid value.
+            long deadline =  Utils.getIntegerNetProperty(
+                "jdk.httpclient.internal.selector.timeout",
+                DEF_NODEADLINE); // millis
+            if (deadline <= 0) deadline = DEF_NODEADLINE;
+            deadline = Math.max(deadline, MIN_NODEADLINE);
+            NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
+        }
+
         private final Selector selector;
         private volatile boolean closed;
-        private final List<AsyncEvent> readyList;
         private final List<AsyncEvent> registrations;
-
-        // Uses a weak reference to the HttpClient owning this
-        // selector: a strong reference prevents its garbage
-        // collection while the thread is running.
-        // We want the thread to exit gracefully when the
-        // HttpClient that owns it gets GC'ed.
-        WeakReference<HttpClientImpl> ownerRef;
+        private final System.Logger debug;
+        private final System.Logger debugtimeout;
+        HttpClientImpl owner;
+        ConnectionPool pool;
 
         SelectorManager(HttpClientImpl ref) throws IOException {
-            super(null, null, "SelectorManager", 0, false);
-            ownerRef = new WeakReference<>(ref);
-            readyList = new ArrayList<>();
+            super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
+            owner = ref;
+            debug = ref.debug;
+            debugtimeout = ref.debugtimeout;
+            pool = ref.connectionPool();
             registrations = new ArrayList<>();
             selector = Selector.open();
         }
 
+        void eventUpdated(AsyncEvent e) throws ClosedChannelException {
+            if (Thread.currentThread() == this) {
+                SelectionKey key = e.channel().keyFor(selector);
+                SelectorAttachment sa = (SelectorAttachment) key.attachment();
+                if (sa != null) sa.register(e);
+            } else {
+                register(e);
+            }
+        }
+
         // This returns immediately. So caller not allowed to send/receive
         // on connection.
-
-        synchronized void register(AsyncEvent e) throws IOException {
+        synchronized void register(AsyncEvent e) {
             registrations.add(e);
             selector.wakeup();
         }
@@ -326,23 +548,34 @@
         }
 
         synchronized void shutdown() {
+            debug.log(Level.DEBUG, "SelectorManager shutting down");
             closed = true;
             try {
                 selector.close();
-            } catch (IOException ignored) { }
+            } catch (IOException ignored) {
+            } finally {
+                owner.stop();
+            }
         }
 
         @Override
         public void run() {
+            List<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
+            List<AsyncEvent> readyList = new ArrayList<>();
             try {
                 while (!Thread.currentThread().isInterrupted()) {
-                    HttpClientImpl client;
                     synchronized (this) {
-                        for (AsyncEvent exchange : registrations) {
-                            SelectableChannel c = exchange.channel();
+                        assert errorList.isEmpty();
+                        assert readyList.isEmpty();
+                        for (AsyncEvent event : registrations) {
+                            if (event instanceof AsyncTriggerEvent) {
+                                readyList.add(event);
+                                continue;
+                            }
+                            SelectableChannel chan = event.channel();
+                            SelectionKey key = null;
                             try {
-                                c.configureBlocking(false);
-                                SelectionKey key = c.keyFor(selector);
+                                key = chan.keyFor(selector);
                                 SelectorAttachment sa;
                                 if (key == null || !key.isValid()) {
                                     if (key != null) {
@@ -351,70 +584,141 @@
                                         // before registering the new event.
                                         selector.selectNow();
                                     }
-                                    sa = new SelectorAttachment(c, selector);
+                                    sa = new SelectorAttachment(chan, selector);
                                 } else {
                                     sa = (SelectorAttachment) key.attachment();
                                 }
-                                sa.register(exchange);
+                                // may throw IOE if channel closed: that's OK
+                                sa.register(event);
+                                if (!chan.isOpen()) {
+                                    throw new IOException("Channel closed");
+                                }
                             } catch (IOException e) {
-                                Log.logError("HttpClientImpl: " + e);
-                                c.close();
-                                // let the exchange deal with it
-                                handleEvent(exchange);
+                                Log.logTrace("HttpClientImpl: " + e);
+                                debug.log(Level.DEBUG, () ->
+                                        "Got " + e.getClass().getName()
+                                                 + " while handling"
+                                                 + " registration events");
+                                chan.close();
+                                // let the event abort deal with it
+                                errorList.add(new Pair<>(event, e));
+                                if (key != null) {
+                                    key.cancel();
+                                    selector.selectNow();
+                                }
                             }
                         }
                         registrations.clear();
+                        selector.selectedKeys().clear();
                     }
 
+                    for (AsyncEvent event : readyList) {
+                        assert event instanceof AsyncTriggerEvent;
+                        event.handle();
+                    }
+                    readyList.clear();
+
+                    for (Pair<AsyncEvent,IOException> error : errorList) {
+                        // an IOException was raised and the channel closed.
+                        handleEvent(error.first, error.second);
+                    }
+                    errorList.clear();
+
                     // Check whether client is still alive, and if not,
                     // gracefully stop this thread
-                    if ((client = ownerRef.get()) == null) {
+                    if (!owner.isReferenced()) {
                         Log.logTrace("HttpClient no longer referenced. Exiting...");
                         return;
                     }
-                    long millis = client.purgeTimeoutsAndReturnNextDeadline();
-                    client = null; // don't hold onto the client ref
+
+                    // Timeouts will have milliseconds granularity. It is important
+                    // to handle them in a timely fashion.
+                    long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
+                    debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
 
-                    //debugPrint(selector);
+                    // Keep-alive have seconds granularity. It's not really an
+                    // issue if we keep connections linger a bit more in the keep
+                    // alive cache.
+                    long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
+                    debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
+
+                    assert nextTimeout >= 0;
+                    assert nextExpiry >= 0;
+
                     // Don't wait for ever as it might prevent the thread to
                     // stop gracefully. millis will be 0 if no deadline was found.
+                    if (nextTimeout <= 0) nextTimeout = NODEADLINE;
+
+                    // Clip nextExpiry at NODEADLINE limit. The default
+                    // keep alive is 1200 seconds (half an hour) - we don't
+                    // want to wait that long.
+                    if (nextExpiry <= 0) nextExpiry = NODEADLINE;
+                    else nextExpiry = Math.min(NODEADLINE, nextExpiry);
+
+                    // takes the least of the two.
+                    long millis = Math.min(nextExpiry, nextTimeout);
+
+                    debugtimeout.log(Level.DEBUG, "Next deadline is %d",
+                                     (millis == 0 ? NODEADLINE : millis));
+                    //debugPrint(selector);
                     int n = selector.select(millis == 0 ? NODEADLINE : millis);
                     if (n == 0) {
                         // Check whether client is still alive, and if not,
                         // gracefully stop this thread
-                        if ((client = ownerRef.get()) == null) {
+                        if (!owner.isReferenced()) {
                             Log.logTrace("HttpClient no longer referenced. Exiting...");
                             return;
                         }
-                        client.purgeTimeoutsAndReturnNextDeadline();
-                        client = null; // don't hold onto the client ref
+                        owner.purgeTimeoutsAndReturnNextDeadline();
                         continue;
                     }
                     Set<SelectionKey> keys = selector.selectedKeys();
 
+                    assert errorList.isEmpty();
                     for (SelectionKey key : keys) {
                         SelectorAttachment sa = (SelectorAttachment) key.attachment();
-                        int eventsOccurred = key.readyOps();
+                        if (!key.isValid()) {
+                            IOException ex = sa.chan.isOpen()
+                                    ? new IOException("Invalid key")
+                                    : new ClosedChannelException();
+                            sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
+                            sa.pending.clear();
+                            continue;
+                        }
+
+                        int eventsOccurred;
+                        try {
+                            eventsOccurred = key.readyOps();
+                        } catch (CancelledKeyException ex) {
+                            IOException io = Utils.getIOException(ex);
+                            sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
+                            sa.pending.clear();
+                            continue;
+                        }
                         sa.events(eventsOccurred).forEach(readyList::add);
                         sa.resetInterestOps(eventsOccurred);
                     }
                     selector.selectNow(); // complete cancellation
                     selector.selectedKeys().clear();
 
-                    for (AsyncEvent exchange : readyList) {
-                        if (exchange.blocking()) {
-                            exchange.channel().configureBlocking(true);
-                        }
-                        handleEvent(exchange); // will be delegated to executor
+                    for (AsyncEvent event : readyList) {
+                        handleEvent(event, null); // will be delegated to executor
                     }
                     readyList.clear();
+                    errorList.forEach((p) -> handleEvent(p.first, p.second));
+                    errorList.clear();
                 }
             } catch (Throwable e) {
+                //e.printStackTrace();
                 if (!closed) {
                     // This terminates thread. So, better just print stack trace
                     String err = Utils.stackTrace(e);
                     Log.logError("HttpClientImpl: fatal error: " + err);
                 }
+                debug.log(Level.DEBUG, "shutting down", e);
+                if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
+                    e.printStackTrace(System.err); // always print the stack
+                }
             } finally {
                 shutdown();
             }
@@ -431,11 +735,12 @@
             System.err.println("Selector: debugprint end");
         }
 
-        void handleEvent(AsyncEvent e) {
-            if (closed) {
-                e.abort();
+        /** Handles the given event. The given ioe may be null. */
+        void handleEvent(AsyncEvent event, IOException ioe) {
+            if (closed || ioe != null) {
+                event.abort(ioe);
             } else {
-                e.handle();
+                event.handle();
             }
         }
     }
@@ -453,11 +758,13 @@
     private static class SelectorAttachment {
         private final SelectableChannel chan;
         private final Selector selector;
-        private final ArrayList<AsyncEvent> pending;
+        private final Set<AsyncEvent> pending;
+        private final static System.Logger debug =
+                Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
         private int interestOps;
 
         SelectorAttachment(SelectableChannel chan, Selector selector) {
-            this.pending = new ArrayList<>();
+            this.pending = new HashSet<>();
             this.chan = chan;
             this.selector = selector;
         }
@@ -506,23 +813,48 @@
 
             this.interestOps = newOps;
             SelectionKey key = chan.keyFor(selector);
-            if (newOps == 0) {
+            if (newOps == 0 && pending.isEmpty()) {
                 key.cancel();
             } else {
-                key.interestOps(newOps);
+                try {
+                    key.interestOps(newOps);
+                } catch (CancelledKeyException x) {
+                    // channel may have been closed
+                    debug.log(Level.DEBUG, "key cancelled for " + chan);
+                    abortPending(x);
+                }
+            }
+        }
+
+        void abortPending(Throwable x) {
+            if (!pending.isEmpty()) {
+                AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
+                pending.clear();
+                IOException io = Utils.getIOException(x);
+                for (AsyncEvent event : evts) {
+                    event.abort(io);
+                }
             }
         }
     }
 
-    @Override
-    public SSLContext sslContext() {
-        Utils.checkNetPermission("getSSLContext");
+    /*package-private*/ SSLContext theSSLContext() {
         return sslContext;
     }
 
     @Override
-    public Optional<SSLParameters> sslParameters() {
-        return Optional.ofNullable(sslParams);
+    public SSLContext sslContext() {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            NetPermission np = new NetPermission("getSSLContext");
+            sm.checkPermission(np);
+        }
+        return sslContext;
+    }
+
+    @Override
+    public SSLParameters sslParameters() {
+        return Utils.copySSLParameters(sslParams);
     }
 
     @Override
@@ -530,9 +862,13 @@
         return Optional.ofNullable(authenticator);
     }
 
+    /*package-private*/ final Executor theExecutor() {
+        return executor;
+    }
+
     @Override
-    public Executor executor() {
-        return executor;
+    public final Optional<Executor> executor() {
+        return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
     }
 
     ConnectionPool connectionPool() {
@@ -558,7 +894,12 @@
     @Override
     public WebSocket.Builder newWebSocketBuilder(URI uri,
                                                  WebSocket.Listener listener) {
-        return new BuilderImpl(this, uri, listener);
+        // Make sure to pass the HttpClientFacade to the web socket builder.
+        // This will ensure that the facade is not released before the
+        // WebSocket has been created, at which point the pendingOperationCount
+        // will have been incremented by the DetachedConnectionChannel
+        // (see PlainHttpConnection.detachChannel())
+        return new BuilderImpl(this.facade(), uri, listener);
     }
 
     @Override
@@ -566,6 +907,17 @@
         return version;
     }
 
+    String dbgString() {
+        return dbgTag;
+    }
+
+    @Override
+    public String toString() {
+        // Used by tests to get the client's id and compute the
+        // name of the SelectorManager thread.
+        return super.toString() + ("(" + id + ")");
+    }
+
     //private final HashMap<String, Boolean> http2NotSupported = new HashMap<>();
 
     boolean getHttp2Allowed() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -28,12 +28,26 @@
 import javax.net.ssl.SSLParameters;
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Flow;
+import jdk.incubator.http.HttpClient.Version;
 import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
 
 /**
  * Wraps socket channel layer and takes care of SSL also.
@@ -42,75 +56,149 @@
  *      PlainHttpConnection: regular direct TCP connection to server
  *      PlainProxyConnection: plain text proxy connection
  *      PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server
- *      SSLConnection: TLS channel direct to server
- *      SSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
+ *      AsyncSSLConnection: TLS channel direct to server
+ *      AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
  */
-abstract class HttpConnection implements Closeable {
+abstract class HttpConnection implements Closeable, AsyncConnection {
 
-    enum Mode {
-        BLOCKING,
-        NON_BLOCKING,
-        ASYNC
-    }
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+    final static System.Logger DEBUG_LOGGER = Utils.getDebugLogger(
+            () -> "HttpConnection(SocketTube(?))", DEBUG);
 
-    protected Mode mode;
-
-    // address we are connected to. Could be a server or a proxy
+    /** The address this connection is connected to. Could be a server or a proxy. */
     final InetSocketAddress address;
-    final HttpClientImpl client;
+    private final HttpClientImpl client;
+    private final TrailingOperations trailingOperations;
 
     HttpConnection(InetSocketAddress address, HttpClientImpl client) {
         this.address = address;
         this.client = client;
+        trailingOperations = new TrailingOperations();
     }
 
-    /**
-     * Public API to this class. addr is the ultimate destination. Any proxies
-     * etc are figured out from the request. Returns an instance of one of the
-     * following
-     *      PlainHttpConnection
-     *      PlainTunnelingConnection
-     *      SSLConnection
-     *      SSLTunnelConnection
-     *
-     * When object returned, connect() or connectAsync() must be called, which
-     * when it returns/completes, the connection is usable for requests.
-     */
-    public static HttpConnection getConnection(
-            InetSocketAddress addr, HttpClientImpl client, HttpRequestImpl request)
-    {
-        return getConnectionImpl(addr, client, request, false);
+    private static final class TrailingOperations {
+        private final Map<CompletableFuture<?>, Boolean> operations =
+                new IdentityHashMap<>();
+        void add(CompletableFuture<?> cf) {
+            synchronized(operations) {
+                cf.whenComplete((r,t)-> remove(cf));
+                operations.put(cf, Boolean.TRUE);
+            }
+        }
+        boolean remove(CompletableFuture<?> cf) {
+            synchronized(operations) {
+                return operations.remove(cf);
+            }
+        }
     }
 
-    /**
-     * Called specifically to get an async connection for HTTP/2 over SSL.
-     */
-    public static HttpConnection getConnection(InetSocketAddress addr,
-        HttpClientImpl client, HttpRequestImpl request, boolean isHttp2) {
-
-        return getConnectionImpl(addr, client, request, isHttp2);
+    final void addTrailingOperation(CompletableFuture<?> cf) {
+        trailingOperations.add(cf);
     }
 
-    public abstract void connect() throws IOException, InterruptedException;
+    final void removeTrailingOperation(CompletableFuture<?> cf) {
+        trailingOperations.remove(cf);
+    }
+
+    final HttpClientImpl client() {
+        return client;
+    }
+
+    //public abstract void connect() throws IOException, InterruptedException;
 
     public abstract CompletableFuture<Void> connectAsync();
 
-    /**
-     * Returns whether this connection is connected to its destination
-     */
+    /** Tells whether, or not, this connection is connected to its destination. */
     abstract boolean connected();
 
+    /** Tells whether, or not, this connection is secure ( over SSL ) */
     abstract boolean isSecure();
 
+    /** Tells whether, or not, this connection is proxied. */
     abstract boolean isProxied();
 
+    /** Tells whether, or not, this connection is open. */
+    final boolean isOpen() {
+        return channel().isOpen() &&
+                (connected() ? !getConnectionFlow().isFinished() : true);
+    }
+
+    interface HttpPublisher extends FlowTube.TubePublisher { }
+
+    /**
+     * Returns the HTTP publisher associated with this connection.  May be null
+     * if invoked before connecting.
+     */
+    abstract HttpPublisher publisher();
+
     /**
-     * Completes when the first byte of the response is available to be read.
+     * Factory for retrieving HttpConnections. A connection can be retrieved
+     * from the connection pool, or a new one created if none available.
+     *
+     * The given {@code addr} is the ultimate destination. Any proxies,
+     * etc, are determined from the request. Returns a concrete instance which
+     * is one of the following:
+     *      {@link PlainHttpConnection}
+     *      {@link PlainTunnelingConnection}
+     *      {@link SSLConnection}
+     *      {@link SSLTunnelConnection}
+     *
+     * The returned connection, if not from the connection pool, must have its,
+     * connect() or connectAsync() method invoked, which ( when it completes
+     * successfully ) renders the connection usable for requests.
      */
-    abstract CompletableFuture<Void> whenReceivingResponse();
+    public static HttpConnection getConnection(InetSocketAddress addr,
+                                               HttpClientImpl client,
+                                               HttpRequestImpl request,
+                                               Version version) {
+        HttpConnection c = null;
+        InetSocketAddress proxy = request.proxy(client);
+        if (proxy != null && proxy.isUnresolved()) {
+            // The default proxy selector may select a proxy whose  address is
+            // unresolved. We must resolve the address before connecting to it.
+            proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
+        }
+        boolean secure = request.secure();
+        ConnectionPool pool = client.connectionPool();
 
-    final boolean isOpen() {
-        return channel().isOpen();
+        if (!secure) {
+            c = pool.getConnection(false, addr, proxy);
+            if (c != null && c.isOpen() /* may have been eof/closed when in the pool */) {
+                final HttpConnection conn = c;
+                DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+                            + ": plain connection retrieved from HTTP/1.1 pool");
+                return c;
+            } else {
+                return getPlainConnection(addr, proxy, request, client);
+            }
+        } else {  // secure
+            if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
+                c = pool.getConnection(true, addr, proxy);
+            }
+            if (c != null && c.isOpen()) {
+                final HttpConnection conn = c;
+                DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+                            + ": SSL connection retrieved from HTTP/1.1 pool");
+                return c;
+            } else {
+                String[] alpn = null;
+                if (version == HTTP_2) {
+                    alpn = new String[] { "h2", "http/1.1" };
+                }
+                return getSSLConnection(addr, proxy, alpn, client);
+            }
+        }
+    }
+
+    private static HttpConnection getSSLConnection(InetSocketAddress addr,
+                                                   InetSocketAddress proxy,
+                                                   String[] alpn,
+                                                   HttpClientImpl client) {
+        if (proxy != null)
+            return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
+        else
+            return new AsyncSSLConnection(addr, client, alpn);
     }
 
     /* Returns either a plain HTTP connection or a plain tunnelling connection
@@ -119,143 +207,48 @@
                                                      InetSocketAddress proxy,
                                                      HttpRequestImpl request,
                                                      HttpClientImpl client) {
-        if (request.isWebSocket() && proxy != null) {
+        if (request.isWebSocket() && proxy != null)
             return new PlainTunnelingConnection(addr, proxy, client);
-        } else {
-            if (proxy == null) {
-                return new PlainHttpConnection(addr, client);
-            } else {
-                return new PlainProxyConnection(proxy, client);
-            }
-        }
-    }
 
-    private static HttpConnection getSSLConnection(InetSocketAddress addr,
-            InetSocketAddress proxy, HttpRequestImpl request,
-            String[] alpn, boolean isHttp2, HttpClientImpl client)
-    {
-        if (proxy != null) {
-            if (!isHttp2) {
-                return new SSLTunnelConnection(addr, client, proxy);
-            } else {
-                return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
-            }
-        } else if (!isHttp2) {
-            return new SSLConnection(addr, client, alpn);
-        } else {
-            return new AsyncSSLConnection(addr, client, alpn);
-        }
+        if (proxy == null)
+            return new PlainHttpConnection(addr, client);
+        else
+            return new PlainProxyConnection(proxy, client);
     }
 
-    /**
-     * Main factory method.   Gets a HttpConnection, either cached or new if
-     * none available.
-     */
-    private static HttpConnection getConnectionImpl(InetSocketAddress addr,
-            HttpClientImpl client,
-            HttpRequestImpl request, boolean isHttp2)
-    {
-        HttpConnection c = null;
-        InetSocketAddress proxy = request.proxy(client);
-        if (proxy != null && proxy.isUnresolved()) {
-            // The default proxy selector may select a proxy whose
-            // address is unresolved. We must resolve the address
-            // before using it to connect.
-            proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
-        }
-        boolean secure = request.secure();
-        ConnectionPool pool = client.connectionPool();
-        String[] alpn =  null;
-
-        if (secure && isHttp2) {
-            alpn = new String[2];
-            alpn[0] = "h2";
-            alpn[1] = "http/1.1";
-        }
-
-        if (!secure) {
-            c = pool.getConnection(false, addr, proxy);
-            if (c != null) {
-                return c;
-            } else {
-                return getPlainConnection(addr, proxy, request, client);
-            }
-        } else {
-            if (!isHttp2) { // if http2 we don't cache connections
-                c = pool.getConnection(true, addr, proxy);
-            }
-            if (c != null) {
-                return c;
-            } else {
-                return getSSLConnection(addr, proxy, request, alpn, isHttp2, client);
-            }
-        }
-    }
-
-    void returnToCache(HttpHeaders hdrs) {
+    void closeOrReturnToCache(HttpHeaders hdrs) {
         if (hdrs == null) {
-            // the connection was closed by server
+            // the connection was closed by server, eof
             close();
             return;
         }
         if (!isOpen()) {
             return;
         }
+        HttpClientImpl client = client();
+        if (client == null) {
+            close();
+            return;
+        }
         ConnectionPool pool = client.connectionPool();
         boolean keepAlive = hdrs.firstValue("Connection")
                 .map((s) -> !s.equalsIgnoreCase("close"))
                 .orElse(true);
 
         if (keepAlive) {
+            Log.logTrace("Returning connection to the pool: {0}", this);
             pool.returnToPool(this);
         } else {
             close();
         }
     }
 
-    /**
-     * Also check that the number of bytes written is what was expected. This
-     * could be different if the buffer is user-supplied and its internal
-     * pointers were manipulated in a race condition.
-     */
-    final void checkWrite(long expected, ByteBuffer buffer) throws IOException {
-        long written = write(buffer);
-        if (written != expected) {
-            throw new IOException("incorrect number of bytes written");
-        }
-    }
-
-    final void checkWrite(long expected,
-                          ByteBuffer[] buffers,
-                          int start,
-                          int length)
-        throws IOException
-    {
-        long written = write(buffers, start, length);
-        if (written != expected) {
-            throw new IOException("incorrect number of bytes written");
-        }
-    }
-
     abstract SocketChannel channel();
 
     final InetSocketAddress address() {
         return address;
     }
 
-    synchronized void configureMode(Mode mode) throws IOException {
-        this.mode = mode;
-        if (mode == Mode.BLOCKING) {
-            channel().configureBlocking(true);
-        } else {
-            channel().configureBlocking(false);
-        }
-    }
-
-    synchronized Mode getMode() {
-        return mode;
-    }
-
     abstract ConnectionPool.CacheKey cacheKey();
 
     // overridden in SSL only
@@ -263,49 +256,6 @@
         return null;
     }
 
-    // Methods to be implemented for Plain TCP and SSL
-
-    abstract long write(ByteBuffer[] buffers, int start, int number)
-        throws IOException;
-
-    abstract long write(ByteBuffer buffer) throws IOException;
-
-    // Methods to be implemented for Plain TCP (async mode) and AsyncSSL
-
-    /**
-     * In {@linkplain Mode#ASYNC async mode}, this method puts buffers at the
-     * end of the send queue; Otherwise, it is equivalent to {@link
-     * #write(ByteBuffer[], int, int) write(buffers, 0, buffers.length)}.
-     * When in async mode, calling this method should later be followed by
-     * subsequent flushAsync invocation.
-     * That allows multiple threads to put buffers into the queue while some other
-     * thread is writing.
-     */
-    abstract void writeAsync(ByteBufferReference[] buffers) throws IOException;
-
-    /**
-     * In {@linkplain Mode#ASYNC async mode}, this method may put
-     * buffers at the beginning of send queue, breaking frames sequence and
-     * allowing to write these buffers before other buffers in the queue;
-     * Otherwise, it is equivalent to {@link
-     * #write(ByteBuffer[], int, int) write(buffers, 0, buffers.length)}.
-     * When in async mode, calling this method should later be followed by
-     * subsequent flushAsync invocation.
-     * That allows multiple threads to put buffers into the queue while some other
-     * thread is writing.
-     */
-    abstract void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException;
-
-    /**
-     * This method should be called after  any writeAsync/writeAsyncUnordered
-     * invocation.
-     * If there is a race to flushAsync from several threads one thread
-     * (race winner) capture flush operation and write the whole queue content.
-     * Other threads (race losers) exits from the method (not blocking)
-     * and continue execution.
-     */
-    abstract void flushAsync() throws IOException;
-
     /**
      * Closes this connection, by returning the socket to its connection pool.
      */
@@ -316,32 +266,142 @@
 
     abstract void shutdownOutput() throws IOException;
 
-    /**
-     * Puts position to limit and limit to capacity so we can resume reading
-     * into this buffer, but if required > 0 then limit may be reduced so that
-     * no more than required bytes are read next time.
-     */
-    static void resumeChannelRead(ByteBuffer buf, int required) {
-        int limit = buf.limit();
-        buf.position(limit);
-        int capacity = buf.capacity() - limit;
-        if (required > 0 && required < capacity) {
-            buf.limit(limit + required);
-        } else {
-            buf.limit(buf.capacity());
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+    abstract static class DetachedConnectionChannel implements Closeable {
+        DetachedConnectionChannel() {}
+        abstract SocketChannel channel();
+        abstract long write(ByteBuffer[] buffers, int start, int number)
+                throws IOException;
+        abstract void shutdownInput() throws IOException;
+        abstract void shutdownOutput() throws IOException;
+        abstract ByteBuffer read() throws IOException;
+        @Override
+        public abstract void close();
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName() + ": " + channel().toString();
         }
     }
 
-    final ByteBuffer read() throws IOException {
-        ByteBuffer b = readImpl();
-        return b;
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+    abstract DetachedConnectionChannel detachChannel();
+
+    abstract FlowTube getConnectionFlow();
+
+    // This queue and publisher are temporary, and only needed because
+    // the calling code still uses writeAsync/flushAsync
+    final class PlainHttpPublisher implements HttpPublisher {
+        final Object reading;
+        PlainHttpPublisher() {
+            this(new Object());
+        }
+        PlainHttpPublisher(Object readingLock) {
+            this.reading = readingLock;
+        }
+        final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
+        volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+        volatile HttpWriteSubscription subscription;
+        final SequentialScheduler writeScheduler =
+                    new SequentialScheduler(this::flushTask);
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+            synchronized (reading) {
+                //assert this.subscription == null;
+                //assert this.subscriber == null;
+                if (subscription == null) {
+                    subscription = new HttpWriteSubscription();
+                }
+                this.subscriber = subscriber;
+            }
+            subscriber.onSubscribe(subscription);
+            signal();
+        }
+
+        void flushTask(DeferredCompleter completer) {
+            try {
+                HttpWriteSubscription sub = subscription;
+                if (sub != null) sub.flush();
+            } finally {
+                completer.complete();
+            }
+        }
+
+        void signal() {
+            writeScheduler.runOrSchedule();
+        }
+
+        final class HttpWriteSubscription implements Flow.Subscription {
+            volatile boolean cancelled;
+            final Demand demand = new Demand();
+
+            @Override
+            public void request(long n) {
+                if (n <= 0) throw new IllegalArgumentException("non-positive request");
+                demand.increase(n);
+                debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
+                            + n + " from "
+                            + getConnectionFlow());
+                writeScheduler.runOrSchedule();
+            }
+
+            @Override
+            public void cancel() {
+                debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
+                          + getConnectionFlow());
+                cancelled = true;
+            }
+
+            void flush() {
+                while (!queue.isEmpty() && demand.tryDecrement()) {
+                    List<ByteBuffer> elem = queue.poll();
+                    debug.log(Level.DEBUG, () -> "HttpPublisher: sending "
+                                + Utils.remaining(elem) + " bytes ("
+                                + elem.size() + " buffers) to "
+                                + getConnectionFlow());
+                    subscriber.onNext(elem);
+                }
+            }
+        }
+
+        public void writeAsync(ByteBufferReference[] buffers) throws IOException {
+            List<ByteBuffer> l = Arrays.asList(ByteBufferReference.toBuffers(buffers));
+            queue.add(l);
+            int bytes = l.stream().mapToInt(ByteBuffer::remaining).sum();
+            debug.log(Level.DEBUG, "added %d bytes to the write queue", bytes);
+        }
+
+        public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
+            // Unordered frames are sent before existing frames.
+            List<ByteBuffer> l = Arrays.asList(ByteBufferReference.toBuffers(buffers));
+            int bytes = l.stream().mapToInt(ByteBuffer::remaining).sum();
+            queue.addFirst(l);
+            debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
+        }
+
+        public void flushAsync() throws IOException {
+            // ### Remove flushAsync
+            // no-op. Should not be needed now with Tube.
+            // Tube.write will initiate the low-level write
+            debug.log(Level.DEBUG, "signalling the publisher of the write queue");
+            signal();
+        }
     }
 
-    /*
-     * Returns a ByteBuffer with the data available at the moment, or null if
-     * reached EOF.
-     */
-    protected abstract ByteBuffer readImpl() throws IOException;
+    String dbgTag = null;
+    final String dbgString() {
+        FlowTube flow = getConnectionFlow();
+        String tag = dbgTag;
+        if (tag == null && flow != null) {
+            dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
+        } else if (tag == null) {
+            tag = this.getClass().getSimpleName() + "(?)";
+        }
+        return tag;
+    }
 
     @Override
     public String toString() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -29,55 +29,123 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.OptionalLong;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Objects.requireNonNull;
 
 /**
  * A read-only view of a set of HTTP headers.
+ *
+ * <p> The methods of this class ( that accept a String header name ), and the
+ * Map returned by the {@linkplain #map() map} method, operate without regard to
+ * case when retrieving the header value.
+ *
+ * <p> HttpHeaders instances are immutable.
+ *
  * {@Incubating}
  *
  * @since 9
  */
-public interface HttpHeaders {
+public abstract class HttpHeaders {
+
+    /**
+     * Creates an HttpHeaders.
+     */
+    protected HttpHeaders() {}
 
     /**
-     * Returns an {@link java.util.Optional} containing the first value of the
-     * given named (and possibly multi-valued) header. If the header is not
-     * present, then the returned {@code Optional} is empty.
+     * Returns an {@link Optional} containing the first value of the given named
+     * (and possibly multi-valued) header. If the header is not present, then
+     * the returned {@code Optional} is empty.
+     *
+     * @implSpec
+     * The default implementation invokes
+     * {@code allValues(name).stream().findFirst()}
      *
      * @param name the header name
      * @return an {@code Optional<String>} for the first named value
      */
-    public Optional<String> firstValue(String name);
+    public Optional<String> firstValue(String name) {
+        return allValues(name).stream().findFirst();
+    }
 
     /**
-     * Returns an {@link java.util.OptionalLong} containing the first value of the
-     * named header field. If the header is not
-     * present, then the Optional is empty. If the header is present but
-     * contains a value that does not parse as a {@code Long} value, then an
-     * exception is thrown.
+     * Returns an {@link OptionalLong} containing the first value of the
+     * named header field. If the header is not present, then the Optional is
+     * empty. If the header is present but contains a value that does not parse
+     * as a {@code Long} value, then an exception is thrown.
+     *
+     * @implSpec
+     * The default implementation invokes
+     * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()}
      *
      * @param name the header name
      * @return  an {@code OptionalLong}
      * @throws NumberFormatException if a value is found, but does not parse as
      *                               a Long
      */
-    public OptionalLong firstValueAsLong(String name);
+    public OptionalLong firstValueAsLong(String name) {
+        return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
+    }
 
     /**
      * Returns an unmodifiable List of all of the values of the given named
      * header. Always returns a List, which may be empty if the header is not
      * present.
      *
+     * @implSpec
+     * The default implementation invokes, among other things, the
+     * {@code map().get(name)} to retrieve the list of header values.
+     *
      * @param name the header name
      * @return a List of String values
      */
-    public List<String> allValues(String name);
+    public List<String> allValues(String name) {
+        requireNonNull(name);
+        List<String> values = map().get(name);
+        // Making unmodifiable list out of empty in order to make a list which
+        // throws UOE unconditionally
+        return values != null ? values : unmodifiableList(emptyList());
+    }
 
     /**
-     * Returns an unmodifiable multi Map view of this HttpHeaders. This
-     * interface should only be used when it is required to iterate over the
-     * entire set of headers.
+     * Returns an unmodifiable multi Map view of this HttpHeaders.
      *
      * @return the Map
      */
-    public Map<String,List<String>> map();
+    public abstract Map<String, List<String>> map();
+
+    /**
+     * Tests this HTTP headers instance for equality with the given object.
+     *
+     * <p> If the given object is not an {@code HttpHeaders} then this
+     * method returns {@code false}. Two HTTP headers are equal if each
+     * of their corresponding {@linkplain #map() maps} are equal.
+     *
+     * <p> This method satisfies the general contract of the {@link
+     * Object#equals(Object) Object.equals} method.
+     *
+     * @param obj the object to which this object is to be compared
+     * @return {@code true} if, and only if, the given object is an {@code
+     *         HttpHeaders} that is equal to this HTTP headers
+     */
+    public final boolean equals(Object obj) {
+        if (!(obj instanceof HttpHeaders))
+            return false;
+        HttpHeaders that = (HttpHeaders)obj;
+        return this.map().equals(that.map());
+    }
+
+    /**
+     * Computes a hash code for this HTTP headers instance.
+     *
+     * <p> The hash code is based upon the components of the HTTP headers
+     * {@linkplain #map() map}, and satisfies the general contract of the
+     * {@link Object#hashCode Object.hashCode} method.
+     *
+     * @return the hash-code value for this HTTP headers
+     */
+    public final int hashCode() {
+        return map().hashCode();
+    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -28,34 +28,41 @@
 import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.net.URI;
+import java.net.URLPermission;
 import java.nio.ByteBuffer;
-import java.nio.charset.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.time.Duration;
 import java.util.Iterator;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 import java.util.concurrent.Flow;
 import java.util.function.Supplier;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 /**
  * Represents one HTTP request which can be sent to a server.
  * {@Incubating }
  *
- * <p> {@code HttpRequest}s are built from {@code HttpRequest}
- * {@link HttpRequest.Builder builder}s. {@code HttpRequest} builders are
- * obtained by calling {@link HttpRequest#newBuilder(java.net.URI)
- * HttpRequest.newBuilder}.
- * A request's {@link java.net.URI}, headers and body can be set. Request bodies
- * are provided through a {@link BodyProcessor} object supplied to the
- * {@link Builder#DELETE(jdk.incubator.http.HttpRequest.BodyProcessor) DELETE},
- * {@link Builder#POST(jdk.incubator.http.HttpRequest.BodyProcessor) POST} or
- * {@link Builder#PUT(jdk.incubator.http.HttpRequest.BodyProcessor) PUT} methods.
+ * <p> {@code HttpRequest}s are built from {@code HttpRequest} {@link
+ * HttpRequest.Builder builder}s. {@code HttpRequest} builders are obtained by
+ * calling {@link HttpRequest#newBuilder(URI) HttpRequest.newBuilder}. A
+ * request's {@linkplain URI}, headers and body can be set. Request bodies are
+ * provided through a {@link BodyPublisher} object supplied to the
+ * {@link Builder#DELETE(BodyPublisher) DELETE},
+ * {@link Builder#POST(BodyPublisher) POST} or
+ * {@link Builder#PUT(BodyPublisher) PUT} methods.
  * {@link Builder#GET() GET} does not take a body. Once all required
  * parameters have been set in the builder, {@link Builder#build() } is called
- * to return the {@code HttpRequest}. Builders can also be copied
- * and modified multiple times in order to build multiple related requests that
- * differ in some parameters.
+ * to return the {@code HttpRequest}. Builders can also be copied and modified
+ * multiple times in order to build multiple related requests that differ in
+ * some parameters.
  *
  * <p> Two simple, example HTTP interactions are shown below:
  * <pre>
@@ -79,7 +86,7 @@
  *          HttpRequest
  *              .newBuilder(new URI("http://www.foo.com/"))
  *              .headers("Foo", "foovalue", "Bar", "barvalue")
- *              .POST(BodyProcessor.fromString("Hello world"))
+ *              .POST(BodyPublisher.fromString("Hello world"))
  *              .build(),
  *          BodyHandler.asFile(Paths.get("/path"))
  *      );
@@ -87,6 +94,7 @@
  *      Path body = response.body(); // should be "/path"
  * }
  * </pre>
+ *
  * <p> The request is sent and the response obtained by calling one of the
  * following methods in {@link HttpClient}.
  * <ul><li>{@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler)} blocks
@@ -95,7 +103,7 @@
  * request and receives the response asynchronously. Returns immediately with a
  * {@link java.util.concurrent.CompletableFuture CompletableFuture}&lt;{@link
  * HttpResponse}&gt;.</li>
- * <li>{@link HttpClient#sendAsync(HttpRequest,HttpResponse.MultiProcessor) }
+ * <li>{@link HttpClient#sendAsync(HttpRequest, HttpResponse.MultiSubscriber) }
  * sends the request asynchronously, expecting multiple responses. This
  * capability is of most relevance to HTTP/2 server push, but can be used for
  * single responses (HTTP/1.1 or HTTP/2) also.</li>
@@ -109,34 +117,36 @@
  *
  * <p> <b>Request bodies</b>
  *
- * <p> Request bodies are sent using one of the request processor implementations
- * below provided in {@link HttpRequest.BodyProcessor}, or else a custom implementation can be
- * used.
+ * <p> Request bodies can be sent using one of the convenience request publisher
+ * implementations below, provided in {@link BodyPublisher}. Alternatively, a
+ * custom Publisher implementation can be used.
  * <ul>
- * <li>{@link BodyProcessor#fromByteArray(byte[]) fromByteArray(byte[])} from byte array</li>
- * <li>{@link BodyProcessor#fromByteArrays(Iterable) fromByteArrays(Iterable)}
+ * <li>{@link BodyPublisher#fromByteArray(byte[]) fromByteArray(byte[])} from byte array</li>
+ * <li>{@link BodyPublisher#fromByteArrays(Iterable) fromByteArrays(Iterable)}
  *      from an Iterable of byte arrays</li>
- * <li>{@link BodyProcessor#fromFile(java.nio.file.Path) fromFile(Path)} from the file located
+ * <li>{@link BodyPublisher#fromFile(java.nio.file.Path) fromFile(Path)} from the file located
  *     at the given Path</li>
- * <li>{@link BodyProcessor#fromString(java.lang.String) fromString(String)} from a String </li>
- * <li>{@link BodyProcessor#fromInputStream(Supplier) fromInputStream}({@link Supplier}&lt;
+ * <li>{@link BodyPublisher#fromString(java.lang.String) fromString(String)} from a String </li>
+ * <li>{@link BodyPublisher#fromInputStream(Supplier) fromInputStream}({@link Supplier}&lt;
  *      {@link InputStream}&gt;) from an InputStream obtained from a Supplier</li>
- * <li>{@link BodyProcessor#noBody() } no request body is sent</li>
+ * <li>{@link BodyPublisher#noBody() } no request body is sent</li>
  * </ul>
  *
  * <p> <b>Response bodies</b>
  *
- * <p>Responses bodies are handled at two levels. When sending the request,
- * a response body handler is specified. This is a function ({@link HttpResponse.BodyHandler})
- * which will be called with the response status code and headers, once these are received. This
- * function is then expected to return a {@link HttpResponse.BodyProcessor}
- * {@code <T>} which is then used to read the response body converting it
- * into an instance of T. After this occurs, the response becomes
- * available in a {@link HttpResponse} and {@link HttpResponse#body()} can then
- * be called to obtain the body. Some implementations and examples of usage of both {@link
- * HttpResponse.BodyProcessor} and {@link HttpResponse.BodyHandler}
- * are provided in {@link HttpResponse}:
- * <p><b>Some of the pre-defined body handlers</b><br>
+ * <p> Responses bodies are handled at two levels. When sending the request,
+ * a response body handler is specified. This is a function ({@linkplain
+ * HttpResponse.BodyHandler}) which will be called with the response status code
+ * and headers, once they are received. This function is then expected to return
+ * a {@link HttpResponse.BodySubscriber}{@code <T>} which is then used to read
+ * the response body, converting it into an instance of T. After this occurs,
+ * the response becomes available in a {@link HttpResponse}, and {@link
+ * HttpResponse#body()} can then be called to obtain the actual body. Some
+ * implementations and examples of usage of both {@link
+ * HttpResponse.BodySubscriber} and {@link HttpResponse.BodyHandler} are
+ * provided in {@link HttpResponse}:
+ *
+ * <p> <b>Some of the pre-defined body handlers</b><br>
  * <ul>
  * <li>{@link HttpResponse.BodyHandler#asByteArray() BodyHandler.asByteArray()}
  * stores the body in a byte array</li>
@@ -152,8 +162,8 @@
  *
  * <p> With HTTP/2 it is possible for a server to return a main response and zero
  * or more additional responses (known as server pushes) to a client-initiated
- * request. These are handled using a special response processor called {@link
- * HttpResponse.MultiProcessor}.
+ * request. These are handled using a special response subscriber called {@link
+ * HttpResponse.MultiSubscriber}.
  *
  * <p> <b>Blocking/asynchronous behavior and thread usage</b>
  *
@@ -161,29 +171,38 @@
  * <i>asynchronous</i>. {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) }
  * blocks the calling thread until the request has been sent and the response received.
  *
- * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler)}  is asynchronous and returns
- * immediately with a {@link java.util.concurrent.CompletableFuture}&lt;{@link
- * HttpResponse}&gt; and when this object completes (in a background thread) the
- * response has been received.
+ * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler)} is
+ * asynchronous and returns immediately with a {@link CompletableFuture}&lt;{@link
+ * HttpResponse}&gt; and when this object completes (possibly in a different
+ * thread) the response has been received.
  *
- * <p> {@link HttpClient#sendAsync(HttpRequest,HttpResponse.MultiProcessor)}
+ * <p> {@link HttpClient#sendAsync(HttpRequest, HttpResponse.MultiSubscriber)}
  * is the variant for multi responses and is also asynchronous.
  *
- * <p> {@code CompletableFuture}s can be combined in different ways to declare the
- * dependencies among several asynchronous tasks, while allowing for the maximum
- * level of parallelism to be utilized.
+ * <p> {@code CompletableFuture}s can be combined in different ways to declare
+ * the dependencies among several asynchronous tasks, while allowing for the
+ * maximum level of parallelism to be utilized.
  *
- * <p> <b>Security checks</b>
+ * <p> <a id="securitychecks"></a><b>Security checks</b></a>
  *
  * <p> If a security manager is present then security checks are performed by
- * the sending methods. A {@link java.net.URLPermission} or {@link java.net.SocketPermission} is required to
- * access any destination origin server and proxy server utilised. {@code URLPermission}s
- * should be preferred in policy files over {@code SocketPermission}s given the more
- * limited scope of {@code URLPermission}. Permission is always implicitly granted to a
- * system's default proxies. The {@code URLPermission} form used to access proxies uses
- * a method parameter of {@code "CONNECT"} (for all kinds of proxying) and a url string
- * of the form {@code "socket://host:port"} where host and port specify the proxy's
- * address.
+ * the HTTP Client's sending methods. An appropriate {@link URLPermission} is
+ * required to access the destination origin server, and proxy server if one has
+ * been configured. The {@code URLPermission} form used to access proxies uses a
+ * method parameter of {@code "CONNECT"} (for all kinds of proxying) and a URL
+ * string  of the form {@code "socket://host:port"} where host and port specify
+ * the proxy's address.
+ *
+ * <p> In this implementation, if an explicit {@linkplain
+ * HttpClient.Builder#executor(Executor) executor} has not been set for an
+ * {@code HttpClient}, and a security manager has been installed, then the
+ * default executor will execute asynchronous and dependent tasks in a context
+ * that is granted no permissions. Custom {@linkplain HttpRequest.BodyPublisher
+ * request body publishers}, {@linkplain HttpResponse.BodyHandler response body
+ * handlers}, and {@linkplain HttpResponse.BodySubscriber response body
+ * subscribers}, if executing operations that require privileges, should do so
+ * within an appropriate {@linkplain AccessController#doPrivileged(PrivilegedAction)
+ * privileged context}.
  *
  * <p> <b>Examples</b>
  * <pre>{@code
@@ -193,7 +212,7 @@
  *
  *      HttpRequest request = HttpRequest
  *              .newBuilder(new URI("http://www.foo.com/"))
- *              .POST(BodyProcessor.fromString("Hello world"))
+ *              .POST(BodyPublisher.fromString("Hello world"))
  *              .build();
  *
  *      HttpResponse<Path> response =
@@ -239,8 +258,8 @@
  *      // Use File.exists() to check whether file was successfully downloaded
  * }
  * </pre>
- * <p>
- * Unless otherwise stated, {@code null} parameter values will cause methods
+ *
+ * <p> Unless otherwise stated, {@code null} parameter values will cause methods
  * of this class to throw {@code NullPointerException}.
  *
  * @since 9
@@ -263,7 +282,8 @@
      * builder and returns <i>this</i> (ie. the same instance). The methods are
      * not synchronized and should not be called from multiple threads without
      * external synchronization.
-     * <p>Note, that not all request headers may be set by user code. Some are
+     *
+     * <p> Note, that not all request headers may be set by user code. Some are
      * restricted for security reasons and others such as the headers relating
      * to authentication, redirection and cookie management are managed by
      * specific APIs rather than through directly user set headers.
@@ -286,16 +306,17 @@
          * @param uri the request URI
          * @return this request builder
          * @throws IllegalArgumentException if the {@code URI} scheme is not
-         *         supported.
+         *         supported
          */
         public abstract Builder uri(URI uri);
 
         /**
-         * Request server to acknowledge request before sending request
-         * body. This is disabled by default. If enabled, the server is requested
-         * to send an error response or a {@code 100 Continue} response before the client
-         * sends the request body. This means the request processor for the
-         * request will not be invoked until this interim response is received.
+         * Requests the server to acknowledge the request before sending the
+         * body. This is disabled by default. If enabled, the server is
+         * requested to send an error response or a {@code 100 Continue}
+         * response before the client sends the request body. This means the
+         * request publisher for the request will not be invoked until this
+         * interim response is received.
          *
          * @param enable {@code true} if Expect continue to be sent
          * @return this request builder
@@ -303,11 +324,12 @@
         public abstract Builder expectContinue(boolean enable);
 
         /**
-         * Sets the preferred {@link HttpClient.Version} for this
-         * request. The corresponding {@link HttpResponse} should be checked
-         * for the version that was used. If the version is not set
-         * in a request, then the version requested will be that of the
-         * sending {@link HttpClient}.
+         * Sets the preferred {@link HttpClient.Version} for this request.
+         *
+         * <p> The corresponding {@link HttpResponse} should be checked for the
+         * version that was actually used. If the version is not set in a
+         * request, then the version requested will be that of the sending
+         * {@link HttpClient}.
          *
          * @param version the HTTP protocol version requested
          * @return this request builder
@@ -320,29 +342,24 @@
          * @param name the header name
          * @param value the header value
          * @return this request builder
+         * @throws IllegalArgumentException if the header name or value is not
+         *         valid, see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+         *         RFC 7230 section-3.2</a>
          */
         public abstract Builder header(String name, String value);
 
-//        /**
-//         * Overrides the {@code ProxySelector} set on the request's client for this
-//         * request.
-//         *
-//         * @param proxy the ProxySelector to use
-//         * @return this request builder
-//         */
-//        public abstract Builder proxy(ProxySelector proxy);
-
         /**
          * Adds the given name value pairs to the set of headers for this
-         * request. The supplied {@code String}s must alternate as names and values.
+         * request. The supplied {@code String}s must alternate as header names
+         * and values.
          *
-         * @param headers the list of String name value pairs
+         * @param headers the list of name value pairs
          * @return this request builder
-         * @throws IllegalArgumentException if there is an odd number of
-         *                                  parameters
+         * @throws IllegalArgumentException if there are an odd number of
+         *         parameters, or if a header name or value is not valid, see
+         *         <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+         *         RFC 7230 section-3.2</a>
          */
-        // TODO (spec): consider signature change
-        // public abstract Builder headers(java.util.Map.Entry<String,String>... headers);
         public abstract Builder headers(String... headers);
 
         /**
@@ -358,6 +375,7 @@
          *
          * @param duration the timeout duration
          * @return this request builder
+         * @throws IllegalArgumentException if the duration is non-positive
          */
         public abstract Builder timeout(Duration duration);
 
@@ -368,6 +386,9 @@
          * @param name the header name
          * @param value the header value
          * @return this request builder
+         * @throws IllegalArgumentException if the header name or value is not valid,
+         *         see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
+         *         RFC 7230 section-3.2</a>
          */
         public abstract Builder setHeader(String name, String value);
 
@@ -380,57 +401,61 @@
 
         /**
          * Sets the request method of this builder to POST and sets its
-         * request body processor to the given value.
+         * request body publisher to the given value.
          *
-         * @param body the body processor
+         * @param bodyPublisher the body publisher
          *
          * @return a {@code HttpRequest}
          */
-        public abstract Builder POST(BodyProcessor body);
+        public abstract Builder POST(BodyPublisher bodyPublisher);
 
         /**
          * Sets the request method of this builder to PUT and sets its
-         * request body processor to the given value.
+         * request body publisher to the given value.
          *
-         * @param body the body processor
+         * @param bodyPublisher the body publisher
          *
          * @return a {@code HttpRequest}
          */
-        public abstract Builder PUT(BodyProcessor body);
+        public abstract Builder PUT(BodyPublisher bodyPublisher);
 
         /**
          * Sets the request method of this builder to DELETE and sets its
-         * request body processor to the given value.
+         * request body publisher to the given value.
          *
-         * @param body the body processor
+         * @param bodyPublisher the body publisher
          *
          * @return a {@code HttpRequest}
          */
 
-        public abstract Builder DELETE(BodyProcessor body);
+        public abstract Builder DELETE(BodyPublisher bodyPublisher);
 
         /**
          * Sets the request method and request body of this builder to the
          * given values.
          *
-         * @param body the body processor
+         * @apiNote The {@linkplain #noBody() noBody} request body publisher can
+         * be used where no request body is required or appropriate.
+         *
+         * @param bodyPublisher the body publisher
          * @param method the method to use
          * @return a {@code HttpRequest}
-         * @throws IllegalArgumentException if an unrecognized method is used
+         * @throws IllegalArgumentException if the method is unrecognised
          */
-        public abstract Builder method(String method, BodyProcessor body);
+        public abstract Builder method(String method, BodyPublisher bodyPublisher);
 
         /**
          * Builds and returns a {@link HttpRequest}.
          *
          * @return the request
+         * @throws IllegalStateException if a URI has not been set
          */
         public abstract HttpRequest build();
 
         /**
-         * Returns an exact duplicate copy of this {@code Builder} based on current
-         * state. The new builder can then be modified independently of this
-         * builder.
+         * Returns an exact duplicate copy of this {@code Builder} based on
+         * current state. The new builder can then be modified independently of
+         * this builder.
          *
          * @return an exact copy of this Builder
          */
@@ -458,14 +483,13 @@
     }
 
     /**
-     * Returns an {@code Optional} containing the {@link BodyProcessor}
-     * set on this request. If no {@code BodyProcessor} was set in the
-     * requests's builder, then the {@code Optional} is empty.
+     * Returns an {@code Optional} containing the {@link BodyPublisher} set on
+     * this request. If no {@code BodyPublisher} was set in the requests's
+     * builder, then the {@code Optional} is empty.
      *
-     * @return an {@code Optional} containing this request's
-     *         {@code BodyProcessor}
+     * @return an {@code Optional} containing this request's {@code BodyPublisher}
      */
-    public abstract Optional<BodyProcessor> bodyProcessor();
+    public abstract Optional<BodyPublisher> bodyPublisher();
 
     /**
      * Returns the request method for this request. If not set explicitly,
@@ -476,11 +500,13 @@
     public abstract String method();
 
     /**
-     * Returns the duration for this request.
+     * Returns an {@code Optional} containing this request's timeout duration.
+     * If the timeout duration was not set in the request's builder, then the
+     * {@code Optional} is empty.
      *
-     * @return this requests duration
+     * @return an {@code Optional} containing this request's timeout duration
      */
-    public abstract Duration duration();
+    public abstract Optional<Duration> timeout();
 
     /**
      * Returns this request's {@link HttpRequest.Builder#expectContinue(boolean)
@@ -519,138 +545,187 @@
 
 
     /**
-     * A request body handler which sends no request body.
+     * Two {@code HttpRequest} objects are equal if their URI, method and headers
+     * fields are all equal.
      *
-     * @return a BodyProcessor
+     * @param other
+     * @return true if this is equal to other
      */
-    public static BodyProcessor noBody() {
-        return new RequestProcessors.EmptyProcessor();
+    @Override
+    public final boolean equals(Object other) {
+       if (! (other instanceof HttpRequest))
+           return false;
+       HttpRequest that = (HttpRequest)other;
+       if (!that.method().equals(this.method()))
+           return false;
+       if (!that.uri().equals(this.uri()))
+           return false;
+       if (!that.headers().equals(this.headers()))
+           return false;
+       return true;
+    }
+
+    /**
+     * Returns this object's hash code. The hash code is calculated as the sum of the
+     * hash codes of its method, uri and headers fields.
+     *
+     * @return
+     */
+    public final int hashCode() {
+        return method().hashCode()
+                + uri().hashCode()
+                + headers().hashCode();
     }
 
     /**
-     * A processor which converts high level Java objects into flows of
-     * {@link java.nio.ByteBuffer}s suitable for sending as request bodies.
+     * A request body handler which sends no request body.
+     *
+     * @return a BodyPublisher
+     */
+    public static BodyPublisher noBody() {
+        return new RequestPublishers.EmptyPublisher();
+    }
+
+    /**
+     * A Publisher which converts high level Java objects into flows of
+     * {@linkplain ByteBuffer}s suitable for sending as request bodies.
      * {@Incubating}
-     * <p>
-     * {@code BodyProcessor}s implement {@link Flow.Publisher} which means they
-     * act as a publisher of byte buffers.
-     * <p>
-     * The HTTP client implementation subscribes to the processor in
-     * order to receive the flow of outgoing data buffers. The normal semantics
-     * of {@link Flow.Subscriber} and {@link Flow.Publisher} are implemented
-     * by the library and expected from processor implementations.
-     * Each outgoing request results in one {@code Subscriber} subscribing to the
-     * {@code Publisher} in order to provide the sequence of {@code ByteBuffer}s containing
-     * the request body. {@code ByteBuffer}s must be allocated by the processor,
-     * and must not be accessed after being handed over to the library.
-     * These subscriptions complete normally when the request is fully
-     * sent, and can be canceled or terminated early through error. If a request
+     *
+     * <p> {@code BodyPublisher}s implement {@link Flow.Publisher} which means
+     * they act as a publisher of byte buffers.
+     *
+     * <p> The HTTP client implementation subscribes to the publisher in order
+     * to receive the flow of outgoing data buffers. The normal semantics of
+     * {@link Flow.Subscriber} and {@link Flow.Publisher} are implemented by the
+     * library and are expected from publisher implementations. Each outgoing
+     * request results in one {@code Subscriber} subscribing to the {@code
+     * BodyPublisher} in order to provide the sequence of {@code ByteBuffer}s
+     * containing the request body. {@code ByteBuffer}s must be allocated by the
+     * publisher, and must not be accessed after being handed over to the library.
+     * These subscriptions complete normally when the request is fully sent,
+     * and can be canceled or terminated early through error. If a request
      * needs to be resent for any reason, then a new subscription is created
      * which is expected to generate the same data as before.
+     *
+     * <p> A publisher that reports a {@linkplain #contentLength() content
+     * length} of {@code 0} may not be subscribed to by the HTTP client
+     * implementation, as it has effectively no data to publish.
      */
-    public interface BodyProcessor extends Flow.Publisher<ByteBuffer> {
+    public interface BodyPublisher extends Flow.Publisher<ByteBuffer> {
 
         /**
-         * Returns a request body processor whose body is the given {@code String},
-         * converted using the {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8}
+         * Returns a request body publisher whose body is the given {@code
+         * String}, converted using the {@link StandardCharsets#UTF_8 UTF_8}
          * character set.
          *
          * @param body the String containing the body
-         * @return a BodyProcessor
+         * @return a BodyPublisher
          */
-        static BodyProcessor fromString(String body) {
-            return fromString(body, StandardCharsets.UTF_8);
+        static BodyPublisher fromString(String body) {
+            return fromString(body, UTF_8);
         }
 
         /**
-         * Returns a request body processor whose body is the given {@code String}, converted
-         * using the given character set.
+         * Returns a request body publisher whose body is the given {@code
+         * String}, converted using the given character set.
          *
          * @param s the String containing the body
          * @param charset the character set to convert the string to bytes
-         * @return a BodyProcessor
+         * @return a BodyPublisher
          */
-        static BodyProcessor fromString(String s, Charset charset) {
-            return new RequestProcessors.StringProcessor(s, charset);
+        static BodyPublisher fromString(String s, Charset charset) {
+            return new RequestPublishers.StringPublisher(s, charset);
         }
 
         /**
-         * A request body processor that reads its data from an {@link java.io.InputStream}.
-         * A {@link Supplier} of {@code InputStream} is used in case the request needs
-         * to be sent again as the content is not buffered. The {@code Supplier} may return
-         * {@code null} on subsequent attempts in which case, the request fails.
+         * A request body publisher that reads its data from an {@link
+         * InputStream}. A {@link Supplier} of {@code InputStream} is used in
+         * case the request needs to be repeated, as the content is not buffered.
+         * The {@code Supplier} may return {@code null} on subsequent attempts,
+         * in which case the request fails.
          *
          * @param streamSupplier a Supplier of open InputStreams
-         * @return a BodyProcessor
+         * @return a BodyPublisher
          */
         // TODO (spec): specify that the stream will be closed
-        static BodyProcessor fromInputStream(Supplier<? extends InputStream> streamSupplier) {
-            return new RequestProcessors.InputStreamProcessor(streamSupplier);
+        static BodyPublisher fromInputStream(Supplier<? extends InputStream> streamSupplier) {
+            return new RequestPublishers.InputStreamPublisher(streamSupplier);
         }
 
         /**
-         * Returns a request body processor whose body is the given byte array.
+         * Returns a request body publisher whose body is the given byte array.
          *
          * @param buf the byte array containing the body
-         * @return a BodyProcessor
+         * @return a BodyPublisher
          */
-        static BodyProcessor fromByteArray(byte[] buf) {
-            return new RequestProcessors.ByteArrayProcessor(buf);
+        static BodyPublisher fromByteArray(byte[] buf) {
+            return new RequestPublishers.ByteArrayPublisher(buf);
         }
 
         /**
-         * Returns a request body processor whose body is the content of the given byte
-         * array of {@code length} bytes starting from the specified
+         * Returns a request body publisher whose body is the content of the
+         * given byte array of {@code length} bytes starting from the specified
          * {@code offset}.
          *
          * @param buf the byte array containing the body
          * @param offset the offset of the first byte
          * @param length the number of bytes to use
-         * @return a BodyProcessor
+         * @return a BodyPublisher
+         * @throws IndexOutOfBoundsException if the sub-range is defined to be
+         *                                   out-of-bounds
          */
-        static BodyProcessor fromByteArray(byte[] buf, int offset, int length) {
-            return new RequestProcessors.ByteArrayProcessor(buf, offset, length);
+        static BodyPublisher fromByteArray(byte[] buf, int offset, int length) {
+            Objects.checkFromIndexSize(offset, length, buf.length);
+            return new RequestPublishers.ByteArrayPublisher(buf, offset, length);
         }
 
-        /**
-         * A request body processor that takes data from the contents of a File.
-         *
-         * @param path the path to the file containing the body
-         * @return a BodyProcessor
-         * @throws java.io.FileNotFoundException if path not found
-         */
-        static BodyProcessor fromFile(Path path) throws FileNotFoundException {
-            return new RequestProcessors.FileProcessor(path);
+        private static String pathForSecurityCheck(Path path) {
+            return path.toFile().getPath();
         }
 
         /**
-         * A request body processor that takes data from an {@code Iterable} of byte arrays.
-         * An {@link Iterable} is provided which supplies {@link Iterator} instances.
-         * Each attempt to send the request results in one invocation of the
-         * {@code Iterable}
+         * A request body publisher that takes data from the contents of a File.
+         *
+         * @param path the path to the file containing the body
+         * @return a BodyPublisher
+         * @throws java.io.FileNotFoundException if the path is not found
+         * @throws SecurityException if a security manager has been installed
+         *          and it denies {@link SecurityManager#checkRead(String)
+         *          read access} to the given file
+         */
+        static BodyPublisher fromFile(Path path) throws FileNotFoundException {
+            Objects.requireNonNull(path);
+            SecurityManager sm = System.getSecurityManager();
+            if (sm != null)
+                sm.checkRead(pathForSecurityCheck(path));
+            if (Files.notExists(path))
+                throw new FileNotFoundException(path + " not found");
+            return new RequestPublishers.FilePublisher(path);
+        }
+
+        /**
+         * A request body publisher that takes data from an {@code Iterable}
+         * of byte arrays. An {@link Iterable} is provided which supplies
+         * {@link Iterator} instances. Each attempt to send the request results
+         * in one invocation of the {@code Iterable}.
          *
          * @param iter an Iterable of byte arrays
-         * @return a BodyProcessor
+         * @return a BodyPublisher
          */
-        static BodyProcessor fromByteArrays(Iterable<byte[]> iter) {
-            return new RequestProcessors.IterableProcessor(iter);
+        static BodyPublisher fromByteArrays(Iterable<byte[]> iter) {
+            return new RequestPublishers.IterablePublisher(iter);
         }
         /**
          * Returns the content length for this request body. May be zero
-         * if no request content being sent, greater than zero for a fixed
-         * length content, and less than zero for an unknown content length.
+         * if no request body being sent, greater than zero for a fixed
+         * length content, or less than zero for an unknown content length.
          *
-         * @return the content length for this request body if known
+         * This method may be invoked before the publisher is subscribed to.
+         * This method may be invoked more than once by the HTTP client
+         * implementation, and MUST return the same constant value each time.
+         *
+         * @return the content length for this request body, if known
          */
         long contentLength();
-
-//        /**
-//         * Returns a used {@code ByteBuffer} to this request processor. When the
-//         * HTTP implementation has finished sending the contents of a buffer,
-//         * this method is called to return it to the processor for re-use.
-//         *
-//         * @param buffer a used ByteBuffer
-//         */
-        //void returnBuffer(ByteBuffer buffer);
     }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -26,11 +26,12 @@
 package jdk.incubator.http;
 
 import java.net.URI;
-import jdk.incubator.http.HttpRequest.BodyProcessor;
 import java.time.Duration;
 import java.util.Optional;
+import jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
 import static jdk.incubator.http.internal.common.Utils.isValidName;
 import static jdk.incubator.http.internal.common.Utils.isValidValue;
 
@@ -39,16 +40,13 @@
     private HttpHeadersImpl userHeaders;
     private URI uri;
     private String method;
-    //private HttpClient.Redirect followRedirects;
     private boolean expectContinue;
-    private HttpRequest.BodyProcessor body;
+    private BodyPublisher bodyPublisher;
     private volatile Optional<HttpClient.Version> version;
-    //private final HttpClientImpl client;
-    //private ProxySelector proxy;
     private Duration duration;
 
     public HttpRequestBuilderImpl(URI uri) {
-        //this.client = client;
+        requireNonNull(uri, "uri must be non-null");
         checkURI(uri);
         this.uri = uri;
         this.userHeaders = new HttpHeadersImpl();
@@ -58,31 +56,66 @@
 
     public HttpRequestBuilderImpl() {
         this.userHeaders = new HttpHeadersImpl();
+        this.method = "GET"; // default, as per spec
         this.version = Optional.empty();
     }
 
     @Override
     public HttpRequestBuilderImpl uri(URI uri) {
-        requireNonNull(uri);
+        requireNonNull(uri, "uri must be non-null");
         checkURI(uri);
         this.uri = uri;
         return this;
     }
 
+    private static IllegalArgumentException newIAE(String message, Object... args) {
+        return new IllegalArgumentException(format(message, args));
+    }
+
     private static void checkURI(URI uri) {
-        String scheme = uri.getScheme().toLowerCase();
-        if (!scheme.equals("https") && !scheme.equals("http")) {
-            throw new IllegalArgumentException("invalid URI scheme");
+        String scheme = uri.getScheme();
+        if (scheme == null)
+            throw newIAE("URI with undefined scheme");
+        scheme = scheme.toLowerCase();
+        if (!(scheme.equals("https") || scheme.equals("http"))) {
+            throw newIAE("invalid URI scheme %s", scheme);
+        }
+        if (uri.getHost() == null) {
+            throw newIAE("unsupported URI %s", uri);
         }
     }
-/*
+
     @Override
-    public HttpRequestBuilderImpl followRedirects(HttpClient.Redirect follow) {
-        requireNonNull(follow);
-        this.followRedirects = follow;
+    public HttpRequestBuilderImpl copy() {
+        HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
+        b.userHeaders = this.userHeaders.deepCopy();
+        b.method = this.method;
+        b.expectContinue = this.expectContinue;
+        b.bodyPublisher = bodyPublisher;
+        b.uri = uri;
+        b.duration = duration;
+        b.version = version;
+        return b;
+    }
+
+    private void checkNameAndValue(String name, String value) {
+        requireNonNull(name, "name");
+        requireNonNull(value, "value");
+        if (!isValidName(name)) {
+            throw newIAE("invalid header name:", name);
+        }
+        if (!isValidValue(value)) {
+            throw newIAE("invalid header value:%s", value);
+        }
+    }
+
+    @Override
+    public HttpRequestBuilderImpl setHeader(String name, String value) {
+        checkNameAndValue(name, value);
+        userHeaders.setHeader(name, value);
         return this;
     }
-*/
+
     @Override
     public HttpRequestBuilderImpl header(String name, String value) {
         checkNameAndValue(name, value);
@@ -93,8 +126,8 @@
     @Override
     public HttpRequestBuilderImpl headers(String... params) {
         requireNonNull(params);
-        if (params.length % 2 != 0) {
-            throw new IllegalArgumentException("wrong number of parameters");
+        if (params.length == 0 || params.length % 2 != 0) {
+            throw newIAE("wrong number, %d, of parameters", params.length);
         }
         for (int i = 0; i < params.length; i += 2) {
             String name  = params[i];
@@ -104,45 +137,6 @@
         return this;
     }
 
-    /*
-    @Override
-    public HttpRequestBuilderImpl proxy(ProxySelector proxy) {
-        requireNonNull(proxy);
-        this.proxy = proxy;
-        return this;
-    }
-*/
-    @Override
-    public HttpRequestBuilderImpl copy() {
-        HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
-        b.userHeaders = this.userHeaders.deepCopy();
-        b.method = this.method;
-        //b.followRedirects = this.followRedirects;
-        b.expectContinue = this.expectContinue;
-        b.body = body;
-        b.uri = uri;
-        //b.proxy = proxy;
-        return b;
-    }
-
-    @Override
-    public HttpRequestBuilderImpl setHeader(String name, String value) {
-        checkNameAndValue(name, value);
-        userHeaders.setHeader(name, value);
-        return this;
-    }
-
-    private void checkNameAndValue(String name, String value) {
-        requireNonNull(name, "name");
-        requireNonNull(value, "value");
-        if (!isValidName(name)) {
-            throw new IllegalArgumentException("invalid header name");
-        }
-        if (!isValidValue(value)) {
-            throw new IllegalArgumentException("invalid header value");
-        }
-    }
-
     @Override
     public HttpRequestBuilderImpl expectContinue(boolean enable) {
         expectContinue = enable;
@@ -158,49 +152,58 @@
 
     HttpHeadersImpl headers() {  return userHeaders; }
 
-    //HttpClientImpl client() {return client;}
-
     URI uri() { return uri; }
 
     String method() { return method; }
 
-    //HttpClient.Redirect followRedirects() { return followRedirects; }
-
-    //ProxySelector proxy() { return proxy; }
-
     boolean expectContinue() { return expectContinue; }
 
-    public HttpRequest.BodyProcessor body() { return body; }
+    BodyPublisher bodyPublisher() { return bodyPublisher; }
 
     Optional<HttpClient.Version> version() { return version; }
 
     @Override
-    public HttpRequest.Builder GET() { return method("GET", null); }
+    public HttpRequest.Builder GET() {
+        return method0("GET", null);
+    }
 
     @Override
-    public HttpRequest.Builder POST(BodyProcessor body) {
-        return method("POST", body);
+    public HttpRequest.Builder POST(BodyPublisher body) {
+        return method0("POST", requireNonNull(body));
+    }
+
+    @Override
+    public HttpRequest.Builder DELETE(BodyPublisher body) {
+        return method0("DELETE", requireNonNull(body));
     }
 
     @Override
-    public HttpRequest.Builder DELETE(BodyProcessor body) {
-        return method("DELETE", body);
+    public HttpRequest.Builder PUT(BodyPublisher body) {
+        return method0("PUT", requireNonNull(body));
     }
 
     @Override
-    public HttpRequest.Builder PUT(BodyProcessor body) {
-        return method("PUT", body);
+    public HttpRequest.Builder method(String method, BodyPublisher body) {
+        requireNonNull(method);
+        if (method.equals(""))
+            throw newIAE("illegal method <empty string>");
+        return method0(method, requireNonNull(body));
     }
 
-    @Override
-    public HttpRequest.Builder method(String method, BodyProcessor body) {
-        this.method = requireNonNull(method);
-        this.body = body;
+    private HttpRequest.Builder method0(String method, BodyPublisher body) {
+        assert method != null;
+        assert !method.equals("GET") ? body != null : true;
+        assert !method.equals("");
+        this.method = method;
+        this.bodyPublisher = body;
         return this;
     }
 
     @Override
     public HttpRequest build() {
+        if (uri == null)
+            throw new IllegalStateException("uri is null");
+        assert method != null;
         return new HttpRequestImpl(this);
     }
 
@@ -213,6 +216,6 @@
         return this;
     }
 
-    Duration duration() { return duration; }
+    Duration timeout() { return duration; }
 
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -33,6 +33,8 @@
 import java.net.ProxySelector;
 import java.net.URI;
 import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.time.Duration;
 import java.util.Locale;
 import java.util.Optional;
@@ -46,12 +48,12 @@
     private final URI uri;
     private InetSocketAddress authority; // only used when URI not specified
     private final String method;
-    final BodyProcessor requestProcessor;
+    final BodyPublisher requestPublisher;
     final boolean secure;
     final boolean expectContinue;
     private boolean isWebSocket;
     private AccessControlContext acc;
-    private final Duration duration;
+    private final Duration timeout;  // may be null
     private final Optional<HttpClient.Version> version;
 
     /**
@@ -59,27 +61,24 @@
      */
     public HttpRequestImpl(HttpRequestBuilderImpl builder) {
         String method = builder.method();
-        this.method = method == null? "GET" : method;
+        this.method = method == null ? "GET" : method;
         this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
         this.systemHeaders = new HttpHeadersImpl();
         this.uri = builder.uri();
+        assert uri != null;
         this.expectContinue = builder.expectContinue();
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
-        if (builder.body() == null) {
-            this.requestProcessor = HttpRequest.noBody();
-        } else {
-            this.requestProcessor = builder.body();
-        }
-        this.duration = builder.duration();
+        this.requestPublisher = builder.bodyPublisher();  // may be null
+        this.timeout = builder.timeout();
         this.version = builder.version();
     }
 
     /**
      * Creates an HttpRequestImpl from the given request.
      */
-    public HttpRequestImpl(HttpRequest request) {
+    public HttpRequestImpl(HttpRequest request, AccessControlContext acc) {
         String method = request.method();
-        this.method = method == null? "GET" : method;
+        this.method = method == null ? "GET" : method;
         this.userHeaders = request.headers();
         if (request instanceof HttpRequestImpl) {
             this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
@@ -90,12 +89,12 @@
         this.uri = request.uri();
         this.expectContinue = request.expectContinue();
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
-        if (!request.bodyProcessor().isPresent()) {
-            this.requestProcessor = HttpRequest.noBody();
-        } else {
-            this.requestProcessor = request.bodyProcessor().get();
+        this.requestPublisher = request.bodyPublisher().orElse(null);
+        if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
+            // Restricts the file publisher with the senders ACC, if any
+            ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
         }
-        this.duration = request.duration();
+        this.timeout = request.timeout().orElse(null);
         this.version = request.version();
     }
 
@@ -110,28 +109,34 @@
         this.uri = uri;
         this.expectContinue = other.expectContinue;
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
-        this.requestProcessor = other.requestProcessor;
+        this.requestPublisher = other.requestPublisher;  // may be null
         this.acc = other.acc;
-        this.duration = other.duration;
+        this.timeout = other.timeout;
         this.version = other.version();
     }
 
     /* used for creating CONNECT requests  */
-    HttpRequestImpl(String method, HttpClientImpl client,
-                    InetSocketAddress authority) {
+    HttpRequestImpl(String method, InetSocketAddress authority) {
         // TODO: isWebSocket flag is not specified, but the assumption is that
         // such a request will never be made on a connection that will be returned
         // to the connection pool (we might need to revisit this constructor later)
         this.method = method;
         this.systemHeaders = new HttpHeadersImpl();
         this.userHeaders = ImmutableHeaders.empty();
-        this.uri = URI.create("socket://" + authority.getHostString() + ":" + Integer.toString(authority.getPort()) + "/");
-        this.requestProcessor = HttpRequest.noBody();
+        this.uri = URI.create("socket://" + authority.getHostString() + ":"
+                              + Integer.toString(authority.getPort()) + "/");
+        this.requestPublisher = null;
         this.authority = authority;
         this.secure = false;
         this.expectContinue = false;
-        this.duration = null;
-        this.version = Optional.of(client.version());
+        this.timeout = null;
+        // The CONNECT request sent for tunneling is only used in two cases:
+        //   1. websocket, which only supports HTTP/1.1
+        //   2. SSL tunneling through a HTTP/1.1 proxy
+        // In either case we do not want to upgrade the connection to the proxy.
+        // What we want to possibly upgrade is the tunneled connection to the
+        // target server (so not the CONNECT request itself)
+        this.version = Optional.of(HttpClient.Version.HTTP_1_1);
     }
 
     /**
@@ -166,9 +171,9 @@
         this.systemHeaders = parent.systemHeaders;
         this.expectContinue = parent.expectContinue;
         this.secure = parent.secure;
-        this.requestProcessor = parent.requestProcessor;
+        this.requestPublisher = parent.requestPublisher;
         this.acc = parent.acc;
-        this.duration = parent.duration;
+        this.timeout = parent.timeout;
         this.version = parent.version;
     }
 
@@ -195,9 +200,6 @@
 
     InetSocketAddress proxy(HttpClientImpl client) {
         ProxySelector ps = client.proxy().orElse(null);
-        if (ps == null) {
-            ps = client.proxy().orElse(null);
-        }
         if (ps == null || method.equalsIgnoreCase("CONNECT")) {
             return null;
         }
@@ -215,15 +217,10 @@
         return isWebSocket;
     }
 
-//    /** Returns the follow-redirects setting for this request. */
-//    @Override
-//    public jdk.incubator.http.HttpClient.Redirect followRedirects() {
-//        return followRedirects;
-//    }
-
     @Override
-    public Optional<BodyProcessor> bodyProcessor() {
-        return Optional.of(requestProcessor);
+    public Optional<BodyPublisher> bodyPublisher() {
+        return requestPublisher == null ? Optional.empty()
+                                        : Optional.of(requestPublisher);
     }
 
     /**
@@ -237,14 +234,10 @@
     public URI uri() { return uri; }
 
     @Override
-    public Duration duration() {
-        return duration;
+    public Optional<Duration> timeout() {
+        return timeout == null ? Optional.empty() : Optional.of(timeout);
     }
 
-//    HttpClientImpl client() {
-//        return client;
-//    }
-
     HttpHeaders getUserHeaders() { return userHeaders; }
 
     HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
@@ -261,57 +254,24 @@
         systemHeaders.setHeader(name, value);
     }
 
-//    @Override
-//    public <T> HttpResponse<T>
-//    response(HttpResponse.BodyHandler<T> responseHandler)
-//        throws IOException, InterruptedException
-//    {
-//        if (!sent.compareAndSet(false, true)) {
-//            throw new IllegalStateException("request already sent");
-//        }
-//        MultiExchange<Void,T> mex = new MultiExchange<>(this, responseHandler);
-//        return mex.response();
-//    }
-//
-//    @Override
-//    public <T> CompletableFuture<HttpResponse<T>>
-//    responseAsync(HttpResponse.BodyHandler<T> responseHandler)
-//    {
-//        if (!sent.compareAndSet(false, true)) {
-//            throw new IllegalStateException("request already sent");
-//        }
-//        MultiExchange<Void,T> mex = new MultiExchange<>(this, responseHandler);
-//        return mex.responseAsync(null)
-//                  .thenApply((HttpResponseImpl<T> b) -> (HttpResponse<T>) b);
-//    }
-//
-//    @Override
-//    public <U, T> CompletableFuture<U>
-//    multiResponseAsync(HttpResponse.MultiProcessor<U, T> responseHandler)
-//    {
-//        if (!sent.compareAndSet(false, true)) {
-//            throw new IllegalStateException("request already sent");
-//        }
-//        MultiExchange<U,T> mex = new MultiExchange<>(this, responseHandler);
-//        return mex.multiResponseAsync();
-//    }
-
-    public InetSocketAddress getAddress(HttpClientImpl client) {
+    InetSocketAddress getAddress(HttpClientImpl client) {
         URI uri = uri();
         if (uri == null) {
             return authority();
         }
-        int port = uri.getPort();
-        if (port == -1) {
+        int p = uri.getPort();
+        if (p == -1) {
             if (uri.getScheme().equalsIgnoreCase("https")) {
-                port = 443;
+                p = 443;
             } else {
-                port = 80;
+                p = 80;
             }
         }
-        String host = uri.getHost();
+        final String host = uri.getHost();
+        final int port = p;
         if (proxy(client) == null) {
-            return new InetSocketAddress(host, port);
+            PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
+            return AccessController.doPrivileged(pa);
         } else {
             return InetSocketAddress.createUnresolved(host, port);
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,20 +26,22 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.io.InputStream;
 import java.net.URI;
-import jdk.incubator.http.ResponseProcessors.MultiFile;
-import jdk.incubator.http.ResponseProcessors.MultiProcessorImpl;
+import jdk.incubator.http.ResponseSubscribers.MultiSubscriberImpl;
 import static jdk.incubator.http.internal.common.Utils.unchecked;
 import static jdk.incubator.http.internal.common.Utils.charsetFrom;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
+import java.nio.channels.FileChannel;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
-import java.util.Map;
+import java.security.AccessControlContext;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -52,7 +54,7 @@
  * Represents a response to a {@link HttpRequest}.
  * {@Incubating}
  *
- * <p>A {@code HttpResponse} is available when the response status code and
+ * <p> A {@code HttpResponse} is available when the response status code and
  * headers have been received, and typically after the response body has also
  * been received. This depends on the response body handler provided when
  * sending the request. In all cases, the response body handler is invoked
@@ -61,23 +63,24 @@
  *
  * <p> Methods are provided in this class for accessing the response headers,
  * and response body.
- * <p>
- * <b>Response handlers and processors</b>
- * <p>
- * Response bodies are handled at two levels. Application code supplies a response
- * handler ({@link BodyHandler}) which may examine the response status code
- * and headers, and which then returns a {@link BodyProcessor} to actually read
- * (or discard) the body and convert it into some useful Java object type. The handler
- * can return one of the pre-defined processor types, or a custom processor, or
- * if the body is to be discarded, it can call {@link BodyProcessor#discard(Object)
- * BodyProcessor.discard()} and return a processor which discards the response body.
- * Static implementations of both handlers and processors are provided in
- * {@link BodyHandler BodyHandler} and {@link BodyProcessor BodyProcessor} respectively.
- * In all cases, the handler functions provided are convenience implementations
- * which ignore the supplied status code and
- * headers and return the relevant pre-defined {@code BodyProcessor}.
- * <p>
- * See {@link BodyHandler} for example usage.
+ *
+ * <p><b>Response handlers and subscribers</b>
+ *
+ * <p> Response bodies are handled at two levels. Application code supplies a
+ * response handler ({@link BodyHandler}) which may examine the response status
+ * code and headers, and which then returns a {@link BodySubscriber} to actually
+ * read (or discard) the body and convert it into some useful Java object type.
+ * The handler can return one of the pre-defined subscriber types, or a custom
+ * subscriber, or if the body is to be discarded it can call {@link
+ * BodySubscriber#discard(Object) discard} and return a subscriber which
+ * discards the response body. Static implementations of both handlers and
+ * subscribers are provided in {@linkplain BodyHandler BodyHandler} and
+ * {@linkplain BodySubscriber BodySubscriber} respectively. In all cases, the
+ * handler functions provided are convenience implementations which ignore the
+ * supplied status code and headers and return the relevant pre-defined {@code
+ * BodySubscriber}.
+ *
+ * <p> See {@link BodyHandler} for example usage.
  *
  * @param <T> the response body type
  * @since 9
@@ -120,17 +123,17 @@
 
     /**
      * Returns the received response trailers, if there are any, when they
-     * become available. For many response processor types this will be at the same
-     * time as the {@code HttpResponse} itself is available. In such cases, the
-     * returned {@code CompletableFuture} will be already completed.
+     * become available. For many response subscriber types this will be at the
+     * same time as the {@code HttpResponse} itself is available. In such cases,
+     * the returned {@code CompletableFuture} will be already completed.
      *
      * @return a CompletableFuture of the response trailers (may be empty)
      */
     public abstract CompletableFuture<HttpHeaders> trailers();
 
     /**
-     * Returns the body. Depending on the type of {@code T}, the returned body may
-     * represent the body after it was read (such as {@code byte[]}, or
+     * Returns the body. Depending on the type of {@code T}, the returned body
+     * may represent the body after it was read (such as {@code byte[]}, or
      * {@code String}, or {@code Path}) or it may represent an object with
      * which the body is read, such as an {@link java.io.InputStream}.
      *
@@ -161,18 +164,101 @@
      */
     public abstract HttpClient.Version version();
 
+
+    private static String pathForSecurityCheck(Path path) {
+        return path.toFile().getPath();
+    }
+
+    /** A body handler that is further restricted by a given ACC. */
+    interface UntrustedBodyHandler<T> extends BodyHandler<T> {
+        void setAccessControlContext(AccessControlContext acc);
+    }
+
+    /**
+     * A Path body handler.
+     *
+     * Note: Exists mainly too allow setting of the senders ACC post creation of
+     * the handler.
+     */
+    static class PathBodyHandler implements UntrustedBodyHandler<Path> {
+        private final Path file;
+        private final OpenOption[]openOptions;
+        private volatile AccessControlContext acc;
+
+        PathBodyHandler(Path file, OpenOption... openOptions) {
+            this.file = file;
+            this.openOptions = openOptions;
+        }
+
+        @Override
+        public void setAccessControlContext(AccessControlContext acc) {
+            this.acc = acc;
+        }
+
+        @Override
+        public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+            ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
+                    BodySubscriber.asFileImpl(file, openOptions);
+            bs.setAccessControlContext(acc);
+            return bs;
+        }
+    }
+
+    // Similar to Path body handler, but for file download. Supports setting ACC.
+    static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
+        private final Path directory;
+        private final OpenOption[]openOptions;
+        private volatile AccessControlContext acc;
+
+        FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
+            this.directory = directory;
+            this.openOptions = openOptions;
+        }
+
+        @Override
+        public void setAccessControlContext(AccessControlContext acc) {
+            this.acc = acc;
+        }
+
+        @Override
+        public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+            String dispoHeader = headers.firstValue("Content-Disposition")
+                    .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
+            if (!dispoHeader.startsWith("attachment;")) {
+                throw unchecked(new IOException("Unknown Content-Disposition type"));
+            }
+            int n = dispoHeader.indexOf("filename=");
+            if (n == -1) {
+                throw unchecked(new IOException("Bad Content-Disposition type"));
+            }
+            int lastsemi = dispoHeader.lastIndexOf(';');
+            String disposition;
+            if (lastsemi < n) {
+                disposition = dispoHeader.substring(n + 9);
+            } else {
+                disposition = dispoHeader.substring(n + 9, lastsemi);
+            }
+            Path file = Paths.get(directory.toString(), disposition);
+
+            ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
+                    BodySubscriber.asFileImpl(file, openOptions);
+            bs.setAccessControlContext(acc);
+            return bs;
+        }
+    }
+
     /**
      * A handler for response bodies.
      * {@Incubating}
-     * <p>
-     * This is a function that takes two parameters: the response status code,
-     * and the response headers, and which returns a {@link BodyProcessor}.
+     *
+     * <p> This is a function that takes two parameters: the response status code,
+     * and the response headers, and which returns a {@linkplain BodySubscriber}.
      * The function is always called just before the response body is read. Its
      * implementation may examine the status code or headers and must decide,
      * whether to accept the response body or discard it, and if accepting it,
      * exactly how to handle it.
-     * <p>
-     * Some pre-defined implementations which do not utilize the status code
+     *
+     * <p> Some pre-defined implementations which do not utilize the status code
      * or headers (meaning the body is always accepted) are defined:
      * <ul><li>{@link #asByteArray() }</li>
      * <li>{@link #asByteArrayConsumer(java.util.function.Consumer)
@@ -182,15 +268,15 @@
      * <li>{@link #discard(Object) }</li>
      * <li>{@link #asString(java.nio.charset.Charset)
      * asString(Charset)}</li></ul>
-     * <p>
-     * These implementations return the equivalent {@link BodyProcessor}.
+     *
+     * <p> These implementations return the equivalent {@link BodySubscriber}.
      * Alternatively, the handler can be used to examine the status code
-     * or headers and return different body processors as appropriate.
-     * <p>
-     * <b>Examples of handler usage</b>
-     * <p>
-     * The first example uses one of the predefined handler functions which
-     * ignore the response headers and status, and always process the response
+     * or headers and return different body subscribers as appropriate.
+     *
+     * <p><b>Examples of handler usage</b>
+     *
+     * <p> The first example uses one of the predefined handler functions which
+     * ignores the response headers and status, and always process the response
      * body in the same way.
      * <pre>
      * {@code
@@ -201,11 +287,11 @@
      * }
      * </pre>
      * Note, that even though these pre-defined handlers ignore the status code
-     * and headers, this information is still accessible from the {@code HttpResponse}
-     * when it is returned.
-     * <p>
-     * In the second example, the function returns a different processor depending
-     * on the status code.
+     * and headers, this information is still accessible from the
+     * {@code HttpResponse} when it is returned.
+     *
+     * <p> In the second example, the function returns a different subscriber
+     * depending on the status code.
      * <pre>
      * {@code
      *      HttpResponse<Path> resp1 = HttpRequest
@@ -213,93 +299,134 @@
      *              .GET()
      *              .response(
      *                  (status, headers) -> status == 200
-     *                      ? BodyProcessor.asFile(Paths.get("/tmp/f"))
-     *                      : BodyProcessor.discard(Paths.get("/NULL")));
+     *                      ? BodySubscriber.asFile(Paths.get("/tmp/f"))
+     *                      : BodySubscriber.discard(Paths.get("/NULL")));
      * }
      * </pre>
      *
-     * @param <T> the response body type.
+     * @param <T> the response body type
      */
     @FunctionalInterface
     public interface BodyHandler<T> {
 
         /**
-         * Returns a {@link BodyProcessor BodyProcessor} considering the given response status
-         * code and headers. This method is always called before the body is read
-         * and its implementation can decide to keep the body and store it somewhere
-         * or else discard it, by  returning the {@code BodyProcessor} returned
-         * from {@link BodyProcessor#discard(java.lang.Object) discard()}.
+         * Returns a {@link BodySubscriber BodySubscriber} considering the given
+         * response status code and headers. This method is always called before
+         * the body is read and its implementation can decide to keep the body
+         * and store it somewhere, or else discard it by returning the {@code
+         * BodySubscriber} returned from {@link BodySubscriber#discard(Object)
+         * discard}.
          *
          * @param statusCode the HTTP status code received
          * @param responseHeaders the response headers received
-         * @return a response body handler
+         * @return a body subscriber
          */
-        public BodyProcessor<T> apply(int statusCode, HttpHeaders responseHeaders);
+        public BodySubscriber<T> apply(int statusCode, HttpHeaders responseHeaders);
 
         /**
          * Returns a response body handler which discards the response body and
          * uses the given value as a replacement for it.
          *
          * @param <U> the response body type
-         * @param value the value of U to return as the body
+         * @param value the value of U to return as the body, may be null
          * @return a response body handler
          */
         public static <U> BodyHandler<U> discard(U value) {
-            return (status, headers) -> BodyProcessor.discard(value);
+            return (status, headers) -> BodySubscriber.discard(value);
         }
 
         /**
          * Returns a {@code BodyHandler<String>} that returns a
-         * {@link BodyProcessor BodyProcessor}{@code <String>} obtained from
-         * {@link BodyProcessor#asString(java.nio.charset.Charset)
-         * BodyProcessor.asString(Charset)}. If a charset is provided, the
-         * body is decoded using it. If charset is {@code null} then the processor
-         * tries to determine the character set from the {@code Content-encoding}
-         * header. If that charset is not supported then
-         * {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8} is used.
+         * {@link BodySubscriber BodySubscriber}{@code <String>} obtained from
+         * {@link BodySubscriber#asString(Charset) BodySubscriber.asString(Charset)}.
+         * If a charset is provided, the body is decoded using it. If charset is
+         * {@code null} then the handler tries to determine the character set
+         * from the {@code Content-encoding} header. If that charset is not
+         * supported then {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8}
+         * is used.
          *
-         * @param charset the name of the charset to interpret the body as. If
-         * {@code null} then charset determined from Content-encoding header
+         * @param charset The name of the charset to interpret the body as. If
+         *                {@code null} then the charset is determined from the
+         *                <i>Content-encoding</i> header.
          * @return a response body handler
          */
         public static BodyHandler<String> asString(Charset charset) {
             return (status, headers) -> {
                 if (charset != null) {
-                    return BodyProcessor.asString(charset);
+                    return BodySubscriber.asString(charset);
                 }
-                return BodyProcessor.asString(charsetFrom(headers));
+                return BodySubscriber.asString(charsetFrom(headers));
             };
         }
 
-
-        /**
-         * Returns a {@code BodyHandler<Path>} that returns a
-         * {@link BodyProcessor BodyProcessor}{@code <Path>} obtained from
-         * {@link BodyProcessor#asFile(Path) BodyProcessor.asFile(Path)}.
-         * <p>
-         * When the {@code HttpResponse} object is returned, the body has been completely
-         * written to the file, and {@link #body()} returns a reference to its
-         * {@link Path}.
-         *
-         * @param file the file to store the body in
-         * @return a response body handler
-         */
-        public static BodyHandler<Path> asFile(Path file) {
-            return (status, headers) -> BodyProcessor.asFile(file);
-        }
-
         /**
          * Returns a {@code BodyHandler<Path>} that returns a
-         * {@link BodyProcessor BodyProcessor}&lt;{@link Path}&gt;
+         * {@link BodySubscriber BodySubscriber}{@code <Path>} obtained from
+         * {@link BodySubscriber#asFile(Path, OpenOption...)
+         * BodySubscriber.asFile(Path,OpenOption...)}.
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the file, and {@link #body()} returns a
+         * reference to its {@link Path}.
+         *
+         * @param file the filename to store the body in
+         * @param openOptions any options to use when opening/creating the file
+         * @return a response body handler
+         * @throws SecurityException If a security manager has been installed
+         *          and it denies {@link SecurityManager#checkWrite(String)
+         *          write access} to the file. The {@link
+         *          SecurityManager#checkDelete(String) checkDelete} method is
+         *          invoked to check delete access if the file is opened with
+         *          the {@code DELETE_ON_CLOSE} option.
+         */
+        public static BodyHandler<Path> asFile(Path file, OpenOption... openOptions) {
+            Objects.requireNonNull(file);
+            SecurityManager sm = System.getSecurityManager();
+            if (sm != null) {
+                String fn = pathForSecurityCheck(file);
+                sm.checkWrite(fn);
+                List<OpenOption> opts = Arrays.asList(openOptions);
+                if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+                    sm.checkDelete(fn);
+                if (opts.contains(StandardOpenOption.READ))
+                    sm.checkRead(fn);
+            }
+            return new PathBodyHandler(file, openOptions);
+        }
+
+        /**
+         * Returns a {@code BodyHandler<Path>} that returns a
+         * {@link BodySubscriber BodySubscriber}{@code <Path>} obtained from
+         * {@link BodySubscriber#asFile(Path) BodySubscriber.asFile(Path)}.
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the file, and {@link #body()} returns a
+         * reference to its {@link Path}.
+         *
+         * @param file the file to store the body in
+         * @return a response body handler
+         * @throws SecurityException if a security manager has been installed
+         *          and it denies {@link SecurityManager#checkWrite(String)
+         *          write access} to the file
+         */
+        public static BodyHandler<Path> asFile(Path file) {
+            return BodyHandler.asFile(file, StandardOpenOption.CREATE,
+                                            StandardOpenOption.WRITE);
+        }
+
+        /**
+         * Returns a {@code BodyHandler<Path>} that returns a
+         * {@link BodySubscriber BodySubscriber}&lt;{@link Path}&gt;
          * where the download directory is specified, but the filename is
          * obtained from the {@code Content-Disposition} response header. The
-         * {@code Content-Disposition} header must specify the <i>attachment</i> type
-         * and must also contain a
-         * <i>filename</i> parameter. If the filename specifies multiple path
-         * components only the final component is used as the filename (with the
-         * given directory name). When the {@code HttpResponse} object is
-         * returned, the body has been completely written to the file and {@link
-         * #body()} returns a {@code Path} object for the file. The returned {@code Path} is the
+         * {@code Content-Disposition} header must specify the <i>attachment</i>
+         * type and must also contain a <i>filename</i> parameter. If the
+         * filename specifies multiple path components only the final component
+         * is used as the filename (with the given directory name).
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the file and {@link #body()} returns a
+         * {@code Path} object for the file. The returned {@code Path} is the
          * combination of the supplied directory name and the file name supplied
          * by the server. If the destination directory does not exist or cannot
          * be written to, then the response will fail with an {@link IOException}.
@@ -307,245 +434,333 @@
          * @param directory the directory to store the file in
          * @param openOptions open options
          * @return a response body handler
+         * @throws SecurityException If a security manager has been installed
+         *          and it denies {@link SecurityManager#checkWrite(String)
+         *          write access} to the file. The {@link
+         *          SecurityManager#checkDelete(String) checkDelete} method is
+         *          invoked to check delete access if the file is opened with
+         *          the {@code DELETE_ON_CLOSE} option.
          */
-        public static BodyHandler<Path> asFileDownload(Path directory, OpenOption... openOptions) {
-            return (status, headers) -> {
-                String dispoHeader = headers.firstValue("Content-Disposition")
-                        .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
-                if (!dispoHeader.startsWith("attachment;")) {
-                    throw unchecked(new IOException("Unknown Content-Disposition type"));
-                }
-                int n = dispoHeader.indexOf("filename=");
-                if (n == -1) {
-                    throw unchecked(new IOException("Bad Content-Disposition type"));
-                }
-                int lastsemi = dispoHeader.lastIndexOf(';');
-                String disposition;
-                if (lastsemi < n) {
-                    disposition = dispoHeader.substring(n + 9);
-                } else {
-                    disposition = dispoHeader.substring(n + 9, lastsemi);
-                }
-                Path file = Paths.get(directory.toString(), disposition);
-                return BodyProcessor.asFile(file, openOptions);
-            };
+         //####: check if the dir exists and is writable??
+        public static BodyHandler<Path> asFileDownload(Path directory,
+                                                       OpenOption... openOptions) {
+            Objects.requireNonNull(directory);
+            SecurityManager sm = System.getSecurityManager();
+            if (sm != null) {
+                String fn = pathForSecurityCheck(directory);
+                sm.checkWrite(fn);
+                List<OpenOption> opts = Arrays.asList(openOptions);
+                if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+                    sm.checkDelete(fn);
+                if (opts.contains(StandardOpenOption.READ))
+                    sm.checkRead(fn);
+            }
+            return new FileDownloadBodyHandler(directory, openOptions);
         }
 
         /**
-         * Returns a {@code BodyHandler<Path>} that returns a
-         * {@link BodyProcessor BodyProcessor}{@code <Path>} obtained from
-         * {@link BodyProcessor#asFile(java.nio.file.Path, java.nio.file.OpenOption...)
-         * BodyProcessor.asFile(Path,OpenOption...)}.
-         * <p>
-         * When the {@code HttpResponse} object is returned, the body has been completely
-         * written to the file, and {@link #body()} returns a reference to its
-         * {@link Path}.
+         * Returns a {@code BodyHandler<InputStream>} that returns a
+         * {@link BodySubscriber BodySubscriber}{@code <InputStream>} obtained
+         * from {@link BodySubscriber#asInputStream() BodySubscriber.asInputStream}.
          *
-         * @param file the filename to store the body in
-         * @param openOptions any options to use when opening/creating the file
+         * <p> When the {@code HttpResponse} object is returned, the response
+         * headers will have been completely read, but the body may not have
+         * been fully received yet. The {@link #body()} method returns an
+         * {@link InputStream} from which the body can be read as it is received.
+         *
          * @return a response body handler
          */
-        public static BodyHandler<Path> asFile(Path file, OpenOption... openOptions) {
-            return (status, headers) -> BodyProcessor.asFile(file, openOptions);
+        public static BodyHandler<InputStream> asInputStream() {
+            return (status, headers) -> BodySubscriber.asInputStream();
         }
 
         /**
          * Returns a {@code BodyHandler<Void>} that returns a
-         * {@link BodyProcessor BodyProcessor}{@code <Void>} obtained from
-         * {@link BodyProcessor#asByteArrayConsumer(java.util.function.Consumer)
-         * BodyProcessor.asByteArrayConsumer(Consumer)}.
-         * <p>
-         * When the {@code HttpResponse} object is returned, the body has been completely
-         * written to the consumer.
+         * {@link BodySubscriber BodySubscriber}{@code <Void>} obtained from
+         * {@link BodySubscriber#asByteArrayConsumer(Consumer)
+         * BodySubscriber.asByteArrayConsumer(Consumer)}.
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the consumer.
          *
          * @param consumer a Consumer to accept the response body
          * @return a response body handler
          */
         public static BodyHandler<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
-            return (status, headers) -> BodyProcessor.asByteArrayConsumer(consumer);
+            return (status, headers) -> BodySubscriber.asByteArrayConsumer(consumer);
         }
 
         /**
          * Returns a {@code BodyHandler<byte[]>} that returns a
-         * {@link BodyProcessor BodyProcessor}&lt;{@code byte[]}&gt; obtained
-         * from {@link BodyProcessor#asByteArray() BodyProcessor.asByteArray()}.
-         * <p>
-         * When the {@code HttpResponse} object is returned, the body has been completely
-         * written to the byte array.
+         * {@link BodySubscriber BodySubscriber}&lt;{@code byte[]}&gt; obtained
+         * from {@link BodySubscriber#asByteArray() BodySubscriber.asByteArray()}.
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the byte array.
          *
          * @return a response body handler
          */
         public static BodyHandler<byte[]> asByteArray() {
-            return (status, headers) -> BodyProcessor.asByteArray();
+            return (status, headers) -> BodySubscriber.asByteArray();
         }
 
         /**
          * Returns a {@code BodyHandler<String>} that returns a
-         * {@link BodyProcessor BodyProcessor}{@code <String>} obtained from
-         * {@link BodyProcessor#asString(java.nio.charset.Charset)
-         * BodyProcessor.asString(Charset)}. The body is
+         * {@link BodySubscriber BodySubscriber}{@code <String>} obtained from
+         * {@link BodySubscriber#asString(java.nio.charset.Charset)
+         * BodySubscriber.asString(Charset)}. The body is
          * decoded using the character set specified in
          * the {@code Content-encoding} response header. If there is no such
          * header, or the character set is not supported, then
          * {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8} is used.
-         * <p>
-         * When the {@code HttpResponse} object is returned, the body has been completely
-         * written to the string.
+         *
+         * <p> When the {@code HttpResponse} object is returned, the body has
+         * been completely written to the string.
          *
          * @return a response body handler
          */
         public static BodyHandler<String> asString() {
-            return (status, headers) -> BodyProcessor.asString(charsetFrom(headers));
+            return (status, headers) -> BodySubscriber.asString(charsetFrom(headers));
         }
+
+        /**
+         * Returns a {@code BodyHandler} which, when invoked, returns a {@linkplain
+         * BodySubscriber#buffering(BodySubscriber,int) buffering BodySubscriber}
+         * that buffers data before delivering it to the downstream subscriber.
+         * These {@code BodySubscriber} instances are created by calling
+         * {@linkplain BodySubscriber#buffering(BodySubscriber,int)
+         * BodySubscriber.buffering} with a subscriber obtained from the given
+         * downstream handler and the {@code bufferSize} parameter.
+         *
+         * @param downstreamHandler the downstream handler
+         * @param bufferSize the buffer size parameter passed to {@linkplain
+         *        BodySubscriber#buffering(BodySubscriber,int) BodySubscriber.buffering}
+         * @return a body handler
+         * @throws IllegalArgumentException if {@code bufferSize <= 0}
+         */
+         public static <T> BodyHandler<T> buffering(BodyHandler<T> downstreamHandler,
+                                                    int bufferSize) {
+             if (bufferSize <= 0)
+                 throw new IllegalArgumentException("must be greater than 0");
+             return (status, headers) -> BodySubscriber
+                     .buffering(downstreamHandler.apply(status, headers),
+                                bufferSize);
+         }
     }
 
     /**
-     * A processor for response bodies.
+     * A subscriber for response bodies.
      * {@Incubating}
-     * <p>
-     * The object acts as a {@link Flow.Subscriber}&lt;{@link ByteBuffer}&gt; to
-     * the HTTP client implementation which publishes ByteBuffers containing the
-     * response body. The processor converts the incoming buffers of data to
-     * some user-defined object type {@code T}.
-     * <p>
-     * The {@link #getBody()} method returns a {@link CompletionStage}{@code <T>}
-     * that provides the response body object. The {@code CompletionStage} must
-     * be obtainable at any time. When it completes depends on the nature
-     * of type {@code T}. In many cases, when {@code T} represents the entire body after being
-     * read then it completes after the body has been read. If {@code T} is a streaming
-     * type such as {@link java.io.InputStream} then it completes before the
-     * body has been read, because the calling code uses it to consume the data.
+     *
+     * <p> The object acts as a {@link Flow.Subscriber}&lt;{@link List}&lt;{@link
+     * ByteBuffer}&gt;&gt; to the HTTP client implementation, which publishes
+     * unmodifiable lists of ByteBuffers containing the response body. The Flow
+     * of data, as well as the order of ByteBuffers in the Flow lists, is a
+     * strictly ordered representation of the response body. Both the Lists and
+     * the ByteBuffers, once passed to the subscriber, are no longer used by the
+     * HTTP client. The subscriber converts the incoming buffers of data to some
+     * user-defined object type {@code T}.
+     *
+     * <p> The {@link #getBody()} method returns a {@link CompletionStage}{@code
+     * <T>} that provides the response body object. The {@code CompletionStage}
+     * must be obtainable at any time. When it completes depends on the nature
+     * of type {@code T}. In many cases, when {@code T} represents the entire
+     * body after being read then it completes after the body has been read. If
+     * {@code T} is a streaming type such as {@link java.io.InputStream} then it
+     * completes before the body has been read, because the calling code uses it
+     * to consume the data.
      *
      * @param <T> the response body type
      */
-    public interface BodyProcessor<T>
-            extends Flow.Subscriber<ByteBuffer> {
+    public interface BodySubscriber<T>
+            extends Flow.Subscriber<List<ByteBuffer>> {
 
         /**
-         * Returns a {@code CompletionStage} which when completed will return the
-         * response body object.
+         * Returns a {@code CompletionStage} which when completed will return
+         * the response body object.
          *
          * @return a CompletionStage for the response body
          */
         public CompletionStage<T> getBody();
 
         /**
-         * Returns a body processor which stores the response body as a {@code
+         * Returns a body subscriber which stores the response body as a {@code
          * String} converted using the given {@code Charset}.
-         * <p>
-         * The {@link HttpResponse} using this processor is available after the
-         * entire response has been read.
+         *
+         * <p> The {@link HttpResponse} using this subscriber is available after
+         * the entire response has been read.
          *
          * @param charset the character set to convert the String with
-         * @return a body processor
+         * @return a body subscriber
          */
-        public static BodyProcessor<String> asString(Charset charset) {
-            return new ResponseProcessors.ByteArrayProcessor<>(
+        public static BodySubscriber<String> asString(Charset charset) {
+            return new ResponseSubscribers.ByteArraySubscriber<>(
                     bytes -> new String(bytes, charset)
             );
         }
 
         /**
-         * Returns a {@code BodyProcessor} which stores the response body as a
+         * Returns a {@code BodySubscriber} which stores the response body as a
          * byte array.
-         * <p>
-         * The {@link HttpResponse} using this processor is available after the
-         * entire response has been read.
+         *
+         * <p> The {@link HttpResponse} using this subscriber is available after
+         * the entire response has been read.
          *
-         * @return a body processor
+         * @return a body subscriber
          */
-        public static BodyProcessor<byte[]> asByteArray() {
-            return new ResponseProcessors.ByteArrayProcessor<>(
+        public static BodySubscriber<byte[]> asByteArray() {
+            return new ResponseSubscribers.ByteArraySubscriber<>(
                     Function.identity() // no conversion
             );
         }
 
+        // no security check
+        static BodySubscriber<Path> asFileImpl(Path file, OpenOption... openOptions) {
+            return new ResponseSubscribers.PathSubscriber(file, openOptions);
+        }
+
         /**
-         * Returns a {@code BodyProcessor} which stores the response body in a
+         * Returns a {@code BodySubscriber} which stores the response body in a
          * file opened with the given options and name. The file will be opened
-         * with the given options using
-         * {@link java.nio.channels.FileChannel#open(java.nio.file.Path,java.nio.file.OpenOption...)
-         * FileChannel.open} just before the body is read. Any exception thrown will be returned
-         * or thrown from {@link HttpClient#send(jdk.incubator.http.HttpRequest,
-         * jdk.incubator.http.HttpResponse.BodyHandler) HttpClient::send}
-         * or {@link HttpClient#sendAsync(jdk.incubator.http.HttpRequest,
-         * jdk.incubator.http.HttpResponse.BodyHandler) HttpClient::sendAsync}
-         * as appropriate.
-         * <p>
-         * The {@link HttpResponse} using this processor is available after the
-         * entire response has been read.
+         * with the given options using {@link FileChannel#open(Path,OpenOption...)
+         * FileChannel.open} just before the body is read. Any exception thrown
+         * will be returned or thrown from {@link HttpClient#send(HttpRequest,
+         * BodyHandler) HttpClient::send} or {@link HttpClient#sendAsync(HttpRequest,
+         * BodyHandler) HttpClient::sendAsync} as appropriate.
+         *
+         * <p> The {@link HttpResponse} using this subscriber is available after
+         * the entire response has been read.
          *
          * @param file the file to store the body in
          * @param openOptions the list of options to open the file with
-         * @return a body processor
+         * @return a body subscriber
+         * @throws SecurityException If a security manager has been installed
+         *          and it denies {@link SecurityManager#checkWrite(String)
+         *          write access} to the file. The {@link
+         *          SecurityManager#checkDelete(String) checkDelete} method is
+         *          invoked to check delete access if the file is opened with the
+         *          {@code DELETE_ON_CLOSE} option.
          */
-        public static BodyProcessor<Path> asFile(Path file, OpenOption... openOptions) {
-            return new ResponseProcessors.PathProcessor(file, openOptions);
+        public static BodySubscriber<Path> asFile(Path file, OpenOption... openOptions) {
+            Objects.requireNonNull(file);
+            SecurityManager sm = System.getSecurityManager();
+            if (sm != null) {
+                String fn = pathForSecurityCheck(file);
+                sm.checkWrite(fn);
+                List<OpenOption> opts = Arrays.asList(openOptions);
+                if (opts.contains(StandardOpenOption.DELETE_ON_CLOSE))
+                    sm.checkDelete(fn);
+                if (opts.contains(StandardOpenOption.READ))
+                    sm.checkRead(fn);
+            }
+            return asFileImpl(file, openOptions);
         }
 
         /**
-         * Returns a {@code BodyProcessor} which provides the incoming body
-         * data to the provided Consumer of {@code Optional<byte[]>}. Each
-         * call to {@link Consumer#accept(java.lang.Object) Consumer.accept()}
-         * will contain a non empty {@code Optional}, except for the final invocation after
-         * all body data has been read, when the {@code Optional} will be empty.
-         * <p>
-         * The {@link HttpResponse} using this processor is available after the
-         * entire response has been read.
+         * Returns a {@code BodySubscriber} which stores the response body in a
+         * file opened with the given name. Has the same effect as calling
+         * {@link #asFile(Path, OpenOption...) asFile} with the standard open
+         * options {@code CREATE} and {@code WRITE}
+         *
+         * <p> The {@link HttpResponse} using this subscriber is available after
+         * the entire response has been read.
          *
-         * @param consumer a Consumer of byte arrays
-         * @return a BodyProcessor
+         * @param file the file to store the body in
+         * @return a body subscriber
+         * @throws SecurityException if a security manager has been installed
+         *          and it denies {@link SecurityManager#checkWrite(String)
+         *          write access} to the file
          */
-        public static BodyProcessor<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
-            return new ResponseProcessors.ConsumerProcessor(consumer);
+        public static BodySubscriber<Path> asFile(Path file) {
+            return asFile(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
         }
 
         /**
-         * Returns a {@code BodyProcessor} which stores the response body in a
-         * file opened with the given name. Has the same effect as calling
-         * {@link #asFile(java.nio.file.Path, java.nio.file.OpenOption...) asFile}
-         * with the standard open options {@code CREATE} and {@code WRITE}
-         * <p>
-         * The {@link HttpResponse} using this processor is available after the
-         * entire response has been read.
+         * Returns a {@code BodySubscriber} which provides the incoming body
+         * data to the provided Consumer of {@code Optional<byte[]>}. Each
+         * call to {@link Consumer#accept(java.lang.Object) Consumer.accept()}
+         * will contain a non empty {@code Optional}, except for the final
+         * invocation after all body data has been read, when the {@code
+         * Optional} will be empty.
          *
-         * @param file the file to store the body in
-         * @return a body processor
+         * <p> The {@link HttpResponse} using this subscriber is available after
+         * the entire response has been read.
+         *
+         * @param consumer a Consumer of byte arrays
+         * @return a BodySubscriber
          */
-        public static BodyProcessor<Path> asFile(Path file) {
-            return new ResponseProcessors.PathProcessor(
-                    file,
-                    StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+        public static BodySubscriber<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer) {
+            return new ResponseSubscribers.ConsumerSubscriber(consumer);
         }
 
         /**
-         * Returns a response processor which discards the response body. The
+         * Returns a {@code BodySubscriber} which streams the response body as
+         * an {@link InputStream}.
+         *
+         * <p> The {@link HttpResponse} using this subscriber is available
+         * immediately after the response headers have been read, without
+         * requiring to wait for the entire body to be processed. The response
+         * body can then be read directly from the {@link InputStream}.
+         *
+         * @return a body subscriber that streams the response body as an
+         *         {@link InputStream}.
+         */
+        public static BodySubscriber<InputStream> asInputStream() {
+            return new ResponseSubscribers.HttpResponseInputStream();
+        }
+
+        /**
+         * Returns a response subscriber which discards the response body. The
          * supplied value is the value that will be returned from
          * {@link HttpResponse#body()}.
          *
          * @param <U> The type of the response body
-         * @param value the value to return from HttpResponse.body()
-         * @return a {@code BodyProcessor}
+         * @param value the value to return from HttpResponse.body(), may be null
+         * @return a {@code BodySubscriber}
          */
-        public static <U> BodyProcessor<U> discard(U value) {
-            return new ResponseProcessors.NullProcessor<>(Optional.ofNullable(value));
+        public static <U> BodySubscriber<U> discard(U value) {
+            return new ResponseSubscribers.NullSubscriber<>(Optional.ofNullable(value));
         }
+
+        /**
+         * Returns a {@code BodySubscriber} which buffers data before delivering
+         * it to the given downstream subscriber. The subscriber guarantees to
+         * deliver {@code buffersize} bytes of data to each invocation of the
+         * downstream's {@linkplain #onNext(Object) onNext} method, except for
+         * the final invocation, just before {@linkplain #onComplete() onComplete}
+         * is invoked. The final invocation of {@code onNext} may contain fewer
+         * than {@code buffersize} bytes.
+         *
+         * <p> The returned subscriber delegates its {@link #getBody()} method
+         * to the downstream subscriber.
+         *
+         * @param downstream the downstream subscriber
+         * @param bufferSize the buffer size
+         * @return a buffering body subscriber
+         * @throws IllegalArgumentException if {@code bufferSize <= 0}
+         */
+         public static <T> BodySubscriber<T> buffering(BodySubscriber<T> downstream,
+                                                       int bufferSize) {
+             if (bufferSize <= 0)
+                 throw new IllegalArgumentException("must be greater than 0");
+             return new BufferingSubscriber<T>(downstream, bufferSize);
+         }
     }
 
     /**
-     * A response processor for a HTTP/2 multi response.
+     * A response subscriber for a HTTP/2 multi response.
      * {@Incubating}
-     * <p>
-     * A multi response comprises a main response, and zero or more additional
+     *
+     * <p> A multi response comprises a main response, and zero or more additional
      * responses. Each additional response is sent by the server in response to
-     * requests that the server also generates. Additional responses are
+     * requests (PUSH_PROMISEs) that the server also generates. Additional responses are
      * typically resources that the server expects the client will need which
      * are related to the initial request.
      * <p>
      * Note. Instead of implementing this interface, applications should consider
      * first using the mechanism (built on this interface) provided by
-     * {@link MultiProcessor#asMap(java.util.function.Function, boolean)
-     * MultiProcessor.asMap()} which is a slightly simplified, but
+     * {@link MultiSubscriber#asMap(java.util.function.Function, boolean)
+     * MultiSubscriber.asMap()} which is a slightly simplified, but also
      * general purpose interface.
      * <p>
      * The server generated requests are also known as <i>push promises</i>.
@@ -556,7 +771,7 @@
      * the server does not wait for any acknowledgment before sending the
      * response, this must be done quickly to avoid unnecessary data transmission.
      *
-     * <p> {@code MultiProcessor}s are parameterized with a type {@code U} which
+     * <p> {@code MultiSubscriber}s are parameterized with a type {@code U} which
      * represents some meaningful aggregate of the responses received. This
      * would typically be a collection of response or response body objects.
      *
@@ -565,29 +780,40 @@
      *
      * @since 9
      */
-    public interface MultiProcessor<U,T> {
+    public interface MultiSubscriber<U,T> {
         /**
-         * Called for the main request and each push promise that is received.
-         * The first call will always be for the main request that was sent
-         * by the caller. This {@link HttpRequest} parameter
-         * represents the initial request or subsequent PUSH_PROMISE. The
-         * implementation must return an {@code Optional} of {@link BodyHandler} for
-         * the response body. Different handlers (of the same type) can be returned
-         * for different pushes within the same multi send. If no handler
-         * (an empty {@code Optional}) is returned, then the push will be canceled. It is
-         * an error to not return a valid {@code BodyHandler} for the initial (main) request.
+         * Called for the main request from the user. This {@link HttpRequest} parameter
+         * is the request that was supplied to {@link HttpClient#sendAsync(HttpRequest, MultiSubscriber)}. The
+         * implementation must return an {@link BodyHandler} for
+         * the response body.
          *
-         * @param request the main request or subsequent push promise
+         * @param request the request
          *
          * @return an optional body handler
          */
-        Optional<BodyHandler<T>> onRequest(HttpRequest request);
+        BodyHandler<T> onRequest(HttpRequest request);
+
+        /**
+         * Called for each push promise that is received. The {@link HttpRequest} parameter
+         * represents the PUSH_PROMISE. The implementation must return an {@code Optional} of
+         * {@link BodyHandler} for the response body. Different handlers (of the same type) can be returned
+         * for different pushes within the same multi send. If no handler
+         * (an empty {@code Optional}) is returned, then the push will be canceled.
+         * If required, the {@code CompletableFuture<Void>} supplied to the {@code onFinalPushPromise} parameter
+         * of {@link #completion(CompletableFuture, CompletableFuture)} can be used to determine
+         * when the final PUSH_PROMISE is received.
+         *
+         * @param request the push promise
+         *
+         * @return an optional body handler
+         */
+        Optional<BodyHandler<T>> onPushPromise(HttpRequest pushPromise);
 
         /**
          * Called for each response received. For each request either one of
          * onResponse() or onError() is guaranteed to be called, but not both.
          *
-         * [Note] The reason for switching to this callback interface rather
+         * <p> Note: The reason for switching to this callback interface rather
          * than using CompletableFutures supplied to onRequest() is that there
          * is a subtle interaction between those CFs and the CF returned from
          * completion() (or when onComplete() was called formerly). The completion()
@@ -618,6 +844,8 @@
          * on one of the given {@code CompletableFuture<Void}s which themselves complete
          * after all individual responses associated with the multi response
          * have completed, or after all push promises have been received.
+         * This method is called after {@link #onRequest(HttpRequest)} but
+         * before any other methods.
          *
          * @implNote Implementations might follow the pattern shown below
          * <pre>
@@ -653,47 +881,47 @@
          * generated push promise) is returned as a key of the map. The value
          * corresponding to each key is a
          * {@code CompletableFuture<HttpResponse<V>>}.
-         * <p>
-         * There are two ways to use these handlers, depending on the value of
-         * the <i>completion</I> parameter. If completion is true, then the
+         *
+         * <p> There are two ways to use these handlers, depending on the value
+         * of the <i>completion</I> parameter. If completion is true, then the
          * aggregated result will be available after all responses have
          * themselves completed. If <i>completion</i> is false, then the
          * aggregated result will be available immediately after the last push
          * promise was received. In the former case, this implies that all the
          * CompletableFutures in the map values will have completed. In the
          * latter case, they may or may not have completed yet.
-         * <p>
-         * The simplest way to use these handlers is to set completion to
+         *
+         * <p> The simplest way to use these handlers is to set completion to
          * {@code true}, and then all (results) values in the Map will be
          * accessible without blocking.
          * <p>
-         * See {@link #asMap(java.util.function.Function, boolean)
-         * }
+         * See {@link #asMap(java.util.function.Function, boolean)}
          * for a code sample of using this interface.
          *
+         * <p> See {@link #asMap(Function, boolean)} for a code sample of using
+         * this interface.
+         *
          * @param <V> the body type used for all responses
-         * @param pushHandler a function invoked for each request or push
-         * promise
+         * @param reqHandler a function invoked for the user's request and each push promise
          * @param completion {@code true} if the aggregate CompletableFuture
-         * completes after all responses have been received, or {@code false}
-         * after all push promises received.
+         *                   completes after all responses have been received,
+         *                   or {@code false} after all push promises received
          *
-         * @return a MultiProcessor
+         * @return a MultiSubscriber
          */
-        public static <V> MultiProcessor<MultiMapResult<V>,V> asMap(
-            Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> pushHandler,
+        public static <V> MultiSubscriber<MultiMapResult<V>,V> asMap(
+                Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> reqHandler,
                 boolean completion) {
-
-            return new MultiProcessorImpl<V>(pushHandler, completion);
+            return new MultiSubscriberImpl<V>(reqHandler.andThen(optv -> optv.get()), reqHandler, completion);
         }
 
         /**
          * Returns a general purpose handler for multi responses. This is a
-         * convenience method which invokes {@link #asMap(java.util.function.Function,boolean)
+         * convenience method which invokes {@link #asMap(Function,boolean)
          * asMap(Function, true)} meaning that the aggregate result
          * object completes after all responses have been received.
-         * <p>
-         * <b>Example usage:</b>
+         *
+         * <p><b>Example usage:</b>
          * <br>
          * <pre>
          * {@code
@@ -705,26 +933,25 @@
          *          HttpClient client = HttpClient.newHttpClient();
          *
          *          Map<HttpRequest,CompletableFuture<HttpResponse<String>>> results = client
-         *              .sendAsync(request, MultiProcessor.asMap(
+         *              .sendAsync(request, MultiSubscriber.asMap(
          *                  (req) -> Optional.of(HttpResponse.BodyHandler.asString())))
          *              .join();
          * }</pre>
-         * <p>
-         * The lambda in this example is the simplest possible implementation,
+         *
+         * <p> The lambda in this example is the simplest possible implementation,
          * where neither the incoming requests are examined, nor the response
          * headers, and every push that the server sends is accepted. When the
          * join() call returns, all {@code HttpResponse}s and their associated
          * body objects are available.
          *
          * @param <V> the body type used for all responses
-         * @param pushHandler a function invoked for each request or push
-         * promise
-         * @return a MultiProcessor
+         * @param reqHandler a function invoked for each push promise and the main request
+         * @return a MultiSubscriber
          */
-        public static <V> MultiProcessor<MultiMapResult<V>,V> asMap(
-            Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> pushHandler) {
+        public static <V> MultiSubscriber<MultiMapResult<V>,V> asMap(
+                Function<HttpRequest, Optional<HttpResponse.BodyHandler<V>>> reqHandler) {
 
-            return asMap(pushHandler, true);
+            return asMap(reqHandler, true);
         }
 
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -29,6 +29,7 @@
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
 import javax.net.ssl.SSLParameters;
 import jdk.incubator.http.internal.common.Log;
 import jdk.incubator.http.internal.websocket.RawChannel;
@@ -43,11 +44,9 @@
     final HttpRequest initialRequest;
     final HttpRequestImpl finalRequest;
     final HttpHeaders headers;
-    //final HttpHeaders trailers;
     final SSLParameters sslParameters;
     final URI uri;
     final HttpClient.Version version;
-    //final AccessControlContext acc;
     RawChannel rawchan;
     final HttpConnection connection;
     final Stream<T> stream;
@@ -55,14 +54,15 @@
 
     public HttpResponseImpl(HttpRequest initialRequest,
                             Response response,
-                            T body, Exchange<T> exch) {
+                            T body,
+                            Exchange<T> exch) {
         this.responseCode = response.statusCode();
         this.exchange = exch;
         this.initialRequest = initialRequest;
         this.finalRequest = exchange.request();
         this.headers = response.headers();
         //this.trailers = trailers;
-        this.sslParameters = exch.client().sslParameters().orElse(null);
+        this.sslParameters = exch.client().sslParameters();
         this.uri = finalRequest.uri();
         this.version = response.version();
         this.connection = exch.exchImpl.connection();
@@ -162,8 +162,8 @@
             }
             // Http1Exchange may have some remaining bytes in its
             // internal buffer.
-            final ByteBuffer remaining =((Http1Exchange<?>)exchImpl).getBuffer();
-            rawchan = new RawChannelImpl(exchange.client(), connection, remaining);
+            Supplier<ByteBuffer> initial = ((Http1Exchange<?>)exchImpl)::getBuffer;
+            rawchan = new RawChannelImpl(exchange.client(), connection, initial);
         }
         return rawchan;
     }
@@ -173,20 +173,18 @@
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
-    static void logResponse(Response r) {
-        if (!Log.requests()) {
-            return;
-        }
+    @Override
+    public String toString() {
         StringBuilder sb = new StringBuilder();
-        String method = r.request().method();
-        URI uri = r.request().uri();
+        String method = request().method();
+        URI uri = request().uri();
         String uristring = uri == null ? "" : uri.toString();
         sb.append('(')
-                .append(method)
-                .append(" ")
-                .append(uristring)
-                .append(") ")
-                .append(r.statusCode());
-        Log.logResponse(sb.toString());
+          .append(method)
+          .append(" ")
+          .append(uristring)
+          .append(") ")
+          .append(statusCode());
+        return sb.toString();
     }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java	Sun Nov 05 17:32:13 2017 +0000
@@ -38,7 +38,7 @@
 import static java.util.Collections.unmodifiableMap;
 import static java.util.Objects.requireNonNull;
 
-final class ImmutableHeaders implements HttpHeaders {
+final class ImmutableHeaders extends HttpHeaders {
 
     private final Map<String, List<String>> map;
 
@@ -72,25 +72,6 @@
     }
 
     @Override
-    public Optional<String> firstValue(String name) {
-        return allValues(name).stream().findFirst();
-    }
-
-    @Override
-    public OptionalLong firstValueAsLong(String name) {
-        return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
-    }
-
-    @Override
-    public List<String> allValues(String name) {
-        requireNonNull(name);
-        List<String> values = map.get(name);
-        // Making unmodifiable list out of empty in order to make a list which
-        // throws UOE unconditionally
-        return values != null ? values : unmodifiableList(emptyList());
-    }
-
-    @Override
     public Map<String, List<String>> map() {
         return map;
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,22 +26,23 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.time.Duration;
 import java.util.List;
 import java.security.AccessControlContext;
-import java.security.AccessController;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ExecutionException;
-import java.util.function.BiFunction;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.UnaryOperator;
-
+import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
 import jdk.incubator.http.internal.common.Log;
 import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Pair;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
 import jdk.incubator.http.internal.common.Utils;
-import static jdk.incubator.http.internal.common.Pair.pair;
+import static jdk.incubator.http.internal.common.MinimalFuture.completedFuture;
+import static jdk.incubator.http.internal.common.MinimalFuture.failedFuture;
 
 /**
  * Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
@@ -53,18 +54,24 @@
  */
 class MultiExchange<U,T> {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    static final System.Logger DEBUG_LOGGER =
+            Utils.getDebugLogger("MultiExchange"::toString, DEBUG);
+
     private final HttpRequest userRequest; // the user request
     private final HttpRequestImpl request; // a copy of the user request
     final AccessControlContext acc;
     final HttpClientImpl client;
     final HttpResponse.BodyHandler<T> responseHandler;
-    final ExecutorWrapper execWrapper;
     final Executor executor;
-    final HttpResponse.MultiProcessor<U,T> multiResponseHandler;
+    final HttpResponse.MultiSubscriber<U,T> multiResponseSubscriber;
+    final AtomicInteger attempts = new AtomicInteger();
     HttpRequestImpl currentreq; // used for async only
     Exchange<T> exchange; // the current exchange
     Exchange<T> previous;
-    int attempts;
+    volatile Throwable retryCause;
+    volatile boolean expiredOnce;
+
     // Maximum number of times a request will be retried/redirected
     // for any reason
 
@@ -93,24 +100,24 @@
      */
     MultiExchange(HttpRequest req,
                   HttpClientImpl client,
-                  HttpResponse.BodyHandler<T> responseHandler) {
+                  HttpResponse.BodyHandler<T> responseHandler,
+                  AccessControlContext acc) {
         this.previous = null;
         this.userRequest = req;
-        this.request = new HttpRequestImpl(req);
+        this.request = new HttpRequestImpl(req, acc);
         this.currentreq = request;
-        this.attempts = 0;
         this.client = client;
         this.filters = client.filterChain();
-        if (System.getSecurityManager() != null) {
-            this.acc = AccessController.getContext();
-        } else {
-            this.acc = null;
+        this.acc = acc;
+        this.executor = client.theExecutor();
+        this.responseHandler = responseHandler;
+        if (acc != null) {
+            // Restricts the file publisher with the senders ACC, if any
+            if (responseHandler instanceof UntrustedBodyHandler)
+                ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
         }
-        this.execWrapper = new ExecutorWrapper(client.executor(), acc);
-        this.executor = execWrapper.executor();
-        this.responseHandler = responseHandler;
         this.exchange = new Exchange<>(request, this);
-        this.multiResponseHandler = null;
+        this.multiResponseSubscriber = null;
         this.pushGroup = null;
     }
 
@@ -119,62 +126,22 @@
      */
     MultiExchange(HttpRequest req,
                   HttpClientImpl client,
-                  HttpResponse.MultiProcessor<U, T> multiResponseHandler) {
+                  HttpResponse.MultiSubscriber<U, T> multiResponseSubscriber,
+                  AccessControlContext acc) {
         this.previous = null;
         this.userRequest = req;
-        this.request = new HttpRequestImpl(req);
+        this.request = new HttpRequestImpl(req, acc);
         this.currentreq = request;
-        this.attempts = 0;
         this.client = client;
         this.filters = client.filterChain();
-        if (System.getSecurityManager() != null) {
-            this.acc = AccessController.getContext();
-        } else {
-            this.acc = null;
-        }
-        this.execWrapper = new ExecutorWrapper(client.executor(), acc);
-        this.executor = execWrapper.executor();
-        this.multiResponseHandler = multiResponseHandler;
-        this.pushGroup = new PushGroup<>(multiResponseHandler, request);
+        this.acc = acc;
+        this.executor = client.theExecutor();
+        this.multiResponseSubscriber = multiResponseSubscriber;
+        this.pushGroup = new PushGroup<>(multiResponseSubscriber, request, acc);
         this.exchange = new Exchange<>(request, this);
         this.responseHandler = pushGroup.mainResponseHandler();
     }
 
-    public HttpResponseImpl<T> response() throws IOException, InterruptedException {
-        HttpRequestImpl r = request;
-        if (r.duration() != null) {
-            timedEvent = new TimedEvent(r.duration());
-            client.registerTimer(timedEvent);
-        }
-        while (attempts < max_attempts) {
-            try {
-                attempts++;
-                Exchange<T> currExchange = getExchange();
-                requestFilters(r);
-                Response response = currExchange.response();
-                HttpRequestImpl newreq = responseFilters(response);
-                if (newreq == null) {
-                    if (attempts > 1) {
-                        Log.logError("Succeeded on attempt: " + attempts);
-                    }
-                    T body = currExchange.readBody(responseHandler);
-                    cancelTimer();
-                    return new HttpResponseImpl<>(userRequest, response, body, currExchange);
-                }
-                //response.body(HttpResponse.ignoreBody());
-                setExchange(new Exchange<>(newreq, this, acc));
-                r = newreq;
-            } catch (IOException e) {
-                if (cancelled) {
-                    throw new HttpTimeoutException("Request timed out");
-                }
-                throw e;
-            }
-        }
-        cancelTimer();
-        throw new IOException("Retry limit exceeded");
-    }
-
     CompletableFuture<Void> multiCompletionCF() {
         return pushGroup.groupResult();
     }
@@ -196,6 +163,9 @@
     }
 
     private synchronized void setExchange(Exchange<T> exchange) {
+        if (this.exchange != null && exchange != this.exchange) {
+            this.exchange.released();
+        }
         this.exchange = exchange;
     }
 
@@ -239,104 +209,103 @@
         getExchange().cancel(cause);
     }
 
-    public CompletableFuture<HttpResponseImpl<T>> responseAsync() {
+    public CompletableFuture<HttpResponse<T>> responseAsync() {
         CompletableFuture<Void> start = new MinimalFuture<>();
-        CompletableFuture<HttpResponseImpl<T>> cf = responseAsync0(start);
+        CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
         start.completeAsync( () -> null, executor); // trigger execution
         return cf;
     }
 
-    private CompletableFuture<HttpResponseImpl<T>> responseAsync0(CompletableFuture<Void> start) {
+    private CompletableFuture<HttpResponse<T>>
+    responseAsync0(CompletableFuture<Void> start) {
         return start.thenCompose( v -> responseAsyncImpl())
-            .thenCompose((Response r) -> {
-                Exchange<T> exch = getExchange();
-                return exch.readBodyAsync(responseHandler)
-                        .thenApply((T body) ->  new HttpResponseImpl<>(userRequest, r, body, exch));
-            });
+                    .thenCompose((Response r) -> {
+                        Exchange<T> exch = getExchange();
+                        return exch.readBodyAsync(responseHandler)
+                                   .thenApply((T body) ->
+                                           new HttpResponseImpl<>(userRequest,
+                                                                  r,
+                                                                  body,
+                                                                  exch));
+                    });
     }
 
     CompletableFuture<U> multiResponseAsync() {
         CompletableFuture<Void> start = new MinimalFuture<>();
-        CompletableFuture<HttpResponseImpl<T>> cf = responseAsync0(start);
+        CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
         CompletableFuture<HttpResponse<T>> mainResponse =
-                cf.thenApply((HttpResponseImpl<T> b) -> {
-                      multiResponseHandler.onResponse(b);
-                      return (HttpResponse<T>)b;
-                   });
-
+                cf.thenApply(b -> {
+                        multiResponseSubscriber.onResponse(b);
+                        pushGroup.noMorePushes(true);
+                        return b; });
         pushGroup.setMainResponse(mainResponse);
-        // set up house-keeping related to multi-response
-        mainResponse.thenAccept((r) -> {
-            // All push promises received by now.
-            pushGroup.noMorePushes(true);
-        });
-        CompletableFuture<U> res = multiResponseHandler.completion(pushGroup.groupResult(), pushGroup.pushesCF());
+        CompletableFuture<U> res = multiResponseSubscriber.completion(pushGroup.groupResult(),
+                                                                      pushGroup.pushesCF());
         start.completeAsync( () -> null, executor); // trigger execution
         return res;
     }
 
     private CompletableFuture<Response> responseAsyncImpl() {
         CompletableFuture<Response> cf;
-        if (++attempts > max_attempts) {
-            cf = MinimalFuture.failedFuture(new IOException("Too many retries"));
+        if (attempts.incrementAndGet() > max_attempts) {
+            cf = failedFuture(new IOException("Too many retries", retryCause));
         } else {
-            if (currentreq.duration() != null) {
-                timedEvent = new TimedEvent(currentreq.duration());
+            if (currentreq.timeout().isPresent()) {
+                timedEvent = new TimedEvent(currentreq.timeout().get());
                 client.registerTimer(timedEvent);
             }
             try {
-                // 1. Apply request filters
+                // 1. apply request filters
                 requestFilters(currentreq);
             } catch (IOException e) {
-                return MinimalFuture.failedFuture(e);
+                return failedFuture(e);
             }
             Exchange<T> exch = getExchange();
             // 2. get response
             cf = exch.responseAsync()
-                .thenCompose((Response response) -> {
-                    HttpRequestImpl newrequest = null;
-                    try {
-                        // 3. Apply response filters
-                        newrequest = responseFilters(response);
-                    } catch (IOException e) {
-                        return MinimalFuture.failedFuture(e);
-                    }
-                    // 4. Check filter result and repeat or continue
-                    if (newrequest == null) {
-                        if (attempts > 1) {
-                            Log.logError("Succeeded on attempt: " + attempts);
+                     .thenCompose((Response response) -> {
+                        HttpRequestImpl newrequest;
+                        try {
+                            // 3. apply response filters
+                            newrequest = responseFilters(response);
+                        } catch (IOException e) {
+                            return failedFuture(e);
                         }
-                        return MinimalFuture.completedFuture(response);
-                    } else {
-                        currentreq = newrequest;
-                        setExchange(new Exchange<>(currentreq, this, acc));
-                        //reads body off previous, and then waits for next response
-                        return responseAsyncImpl();
-                    }
-                })
-            // 5. Handle errors and cancel any timer set
-            .handle((response, ex) -> {
-                cancelTimer();
-                if (ex == null) {
-                    assert response != null;
-                    return MinimalFuture.completedFuture(response);
-                }
-                // all exceptions thrown are handled here
-                CompletableFuture<Response> error = getExceptionalCF(ex);
-                if (error == null) {
-                    return responseAsyncImpl();
-                } else {
-                    return error;
-                }
-            })
-            .thenCompose(UnaryOperator.identity());
+                        // 4. check filter result and repeat or continue
+                        if (newrequest == null) {
+                            if (attempts.get() > 1) {
+                                Log.logError("Succeeded on attempt: " + attempts);
+                            }
+                            return completedFuture(response);
+                        } else {
+                            currentreq = newrequest;
+                            expiredOnce = false;
+                            setExchange(new Exchange<>(currentreq, this, acc));
+                            //reads body off previous, and then waits for next response
+                            return responseAsyncImpl();
+                        } })
+                     .handle((response, ex) -> {
+                        // 5. handle errors and cancel any timer set
+                        cancelTimer();
+                        if (ex == null) {
+                            assert response != null;
+                            return completedFuture(response);
+                        }
+                        // all exceptions thrown are handled here
+                        CompletableFuture<Response> errorCF = getExceptionalCF(ex);
+                        if (errorCF == null) {
+                            return responseAsyncImpl();
+                        } else {
+                            return errorCF;
+                        } })
+                     .thenCompose(UnaryOperator.identity());
         }
         return cf;
     }
 
     /**
-     * Take a Throwable and return a suitable CompletableFuture that is
-     * completed exceptionally.
+     * Takes a Throwable and returns a suitable CompletableFuture that is
+     * completed exceptionally, or null.
      */
     private CompletableFuture<Response> getExceptionalCF(Throwable t) {
         if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
@@ -346,8 +315,24 @@
         }
         if (cancelled && t instanceof IOException) {
             t = new HttpTimeoutException("request timed out");
+        } else if (t instanceof ConnectionExpiredException) {
+            // allow the retry mechanism to do its work
+            // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
+            if (t.getCause() != null) retryCause = t.getCause();
+            if (!expiredOnce) {
+                DEBUG_LOGGER.log(Level.DEBUG,
+                    "MultiExchange: ConnectionExpiredException (async): retrying...",
+                    t);
+                expiredOnce = true;
+                return null;
+            } else {
+                DEBUG_LOGGER.log(Level.DEBUG,
+                    "MultiExchange: ConnectionExpiredException (async): already retried once.",
+                    t);
+                if (t.getCause() != null) t = t.getCause();
+            }
         }
-        return MinimalFuture.failedFuture(t);
+        return failedFuture(t);
     }
 
     class TimedEvent extends TimeoutEvent {
@@ -356,6 +341,9 @@
         }
         @Override
         public void handle() {
+            DEBUG_LOGGER.log(Level.DEBUG,
+                    "Cancelling MultiExchange due to timeout for request %s",
+                     request);
             cancel(new HttpTimeoutException("request timed out"));
         }
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiMapResult.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiMapResult.java	Sun Nov 05 17:32:13 2017 +0000
@@ -44,8 +44,8 @@
  * <p>
  * {@link CompletableFuture}&lt;{@code MultiMapResult<V>}&gt;
  * {@link HttpClient#sendAsync(HttpRequest,
- * HttpResponse.MultiProcessor) HttpClient.sendAsync(}{@link
- * HttpResponse.MultiProcessor#asMap(java.util.function.Function)
+ * HttpResponse.MultiSubscriber) HttpClient.sendAsync(}{@link
+ * HttpResponse.MultiSubscriber#asMap(java.util.function.Function)
  * MultiProcessor.asMap(Function)})
  *
  * @param <V> the response body type for all responses
@@ -117,4 +117,3 @@
         return map.entrySet();
     }
 }
-
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,73 +26,45 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.StandardSocketOptions;
 import java.nio.ByteBuffer;
 import java.nio.channels.SelectableChannel;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.SocketChannel;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import jdk.incubator.http.internal.common.AsyncWriteQueue;
 import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
 import jdk.incubator.http.internal.common.Log;
 import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
 
 /**
- * Plain raw TCP connection direct to destination. 2 modes
- * 1) Blocking used by http/1. In this case the connect is actually non
- *    blocking but the request is sent blocking. The first byte of a response
- *    is received non-blocking and the remainder of the response is received
- *    blocking
- * 2) Non-blocking. In this case (for http/2) the connection is actually opened
- *    blocking but all reads and writes are done non-blocking under the
- *    control of a Http2Connection object.
+ * Plain raw TCP connection direct to destination.
+ * The connection operates in asynchronous non-blocking mode.
+ * All reads and writes are done non-blocking.
  */
-class PlainHttpConnection extends HttpConnection implements AsyncConnection {
+class PlainHttpConnection extends HttpConnection {
 
+    private final Object reading = new Object();
     protected final SocketChannel chan;
+    private final FlowTube tube;
+    // The PlainHttpPublisher is a temporary hack needed because we still
+    // use writeAsync/flushAsync
+    private final PlainHttpPublisher writePublisher = new PlainHttpPublisher(reading);
     private volatile boolean connected;
     private boolean closed;
 
     // should be volatile to provide proper synchronization(visibility) action
-    private volatile Consumer<ByteBufferReference> asyncReceiver;
-    private volatile Consumer<Throwable> errorReceiver;
-    private volatile Supplier<ByteBufferReference> readBufferSupplier;
-    private boolean asyncReading;
 
-    private final AsyncWriteQueue asyncOutputQ = new AsyncWriteQueue(this::asyncOutput);
-
-    private final Object reading = new Object();
-
-    @Override
-    public void startReading() {
-        try {
-            synchronized(reading) {
-                asyncReading = true;
-            }
-            client.registerEvent(new ReadEvent());
-        } catch (IOException e) {
-            shutdown();
-        }
-    }
-
-    @Override
-    public void stopAsyncReading() {
-        synchronized(reading) {
-            asyncReading = false;
-        }
-        client.cancelRegistration(chan);
-    }
-
-    class ConnectEvent extends AsyncEvent {
-        CompletableFuture<Void> cf;
+    final class ConnectEvent extends AsyncEvent {
+        private final CompletableFuture<Void> cf;
 
         ConnectEvent(CompletableFuture<Void> cf) {
-            super(AsyncEvent.BLOCKING);
             this.cf = cf;
         }
 
@@ -108,39 +80,53 @@
 
         @Override
         public void handle() {
+            assert !connected : "Already connected";
+            assert !chan.isBlocking() : "Unexpected blocking channel";
             try {
-                chan.finishConnect();
+                debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
+                boolean finished = chan.finishConnect();
+                assert finished : "Expected channel to be connected";
+                debug.log(Level.DEBUG,
+                          "ConnectEvent: connect finished: %s", finished);
+                connected = true;
+                // complete async since the event runs on the SelectorManager thread
+                cf.completeAsync(() -> null, client().theExecutor());
             } catch (IOException e) {
-                cf.completeExceptionally(e);
-                return;
+                client().theExecutor().execute( () -> cf.completeExceptionally(e));
             }
-            connected = true;
-            cf.complete(null);
         }
 
         @Override
-        public void abort() {
+        public void abort(IOException ioe) {
             close();
+            client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
         }
     }
 
     @Override
     public CompletableFuture<Void> connectAsync() {
-        CompletableFuture<Void> plainFuture = new MinimalFuture<>();
+        assert !connected : "Already connected";
+        assert !chan.isBlocking() : "Unexpected blocking channel";
+        CompletableFuture<Void> cf = new MinimalFuture<>();
         try {
-            chan.configureBlocking(false);
-            chan.connect(address);
-            client.registerEvent(new ConnectEvent(plainFuture));
-        } catch (IOException e) {
-            plainFuture.completeExceptionally(e);
+            boolean finished = false;
+            PrivilegedExceptionAction<Boolean> pa = () -> chan.connect(address);
+            try {
+                 finished = AccessController.doPrivileged(pa);
+            } catch (PrivilegedActionException e) {
+                cf.completeExceptionally(e.getCause());
+            }
+            if (finished) {
+                debug.log(Level.DEBUG, "connect finished without blocking");
+                cf.complete(null);
+            } else {
+                debug.log(Level.DEBUG, "registering connect event");
+                client().registerEvent(new ConnectEvent(cf));
+            }
+        } catch (Throwable throwable) {
+            cf.completeExceptionally(throwable);
         }
-        return plainFuture;
-    }
-
-    @Override
-    public void connect() throws IOException {
-        chan.connect(address);
-        connected = true;
+        return cf;
     }
 
     @Override
@@ -148,106 +134,44 @@
         return chan;
     }
 
+    @Override
+    final FlowTube getConnectionFlow() {
+        return tube;
+    }
+
     PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client) {
         super(addr, client);
         try {
             this.chan = SocketChannel.open();
+            chan.configureBlocking(false);
             int bufsize = client.getReceiveBufferSize();
             chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
             chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
+            // wrap the connected channel in a Tube for async reading and writing
+            tube = new SocketTube(client(), chan, Utils::getBuffer);
         } catch (IOException e) {
             throw new InternalError(e);
         }
     }
 
     @Override
-    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-        if (getMode() != Mode.ASYNC) {
-            return chan.write(buffers, start, number);
-        }
-        // async
-        buffers = Utils.reduce(buffers, start, number);
-        long n = Utils.remaining(buffers);
-        asyncOutputQ.put(ByteBufferReference.toReferences(buffers));
-        flushAsync();
-        return n;
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        if (getMode() != Mode.ASYNC) {
-            return chan.write(buffer);
-        }
-        // async
-        long n = buffer.remaining();
-        asyncOutputQ.put(ByteBufferReference.toReferences(buffer));
-        flushAsync();
-        return n;
-    }
-
-    // handle registered WriteEvent; invoked from SelectorManager thread
-    void flushRegistered() {
-        if (getMode() == Mode.ASYNC) {
-            try {
-                asyncOutputQ.flushDelayed();
-            } catch (IOException e) {
-                // Only IOException caused by closed Queue is expected here
-                shutdown();
-            }
-        }
-    }
+    HttpPublisher publisher() { return writePublisher; }
 
     @Override
     public void writeAsync(ByteBufferReference[] buffers) throws IOException {
-        if (getMode() != Mode.ASYNC) {
-            chan.write(ByteBufferReference.toBuffers(buffers));
-            ByteBufferReference.clear(buffers);
-        } else {
-            asyncOutputQ.put(buffers);
-        }
+        writePublisher.writeAsync(buffers);
     }
 
     @Override
     public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        if (getMode() != Mode.ASYNC) {
-            chan.write(ByteBufferReference.toBuffers(buffers));
-            ByteBufferReference.clear(buffers);
-        } else {
-            // Unordered frames are sent before existing frames.
-            asyncOutputQ.putFirst(buffers);
-        }
+        writePublisher.writeAsyncUnordered(buffers);
     }
 
     @Override
     public void flushAsync() throws IOException {
-        if (getMode() == Mode.ASYNC) {
-            asyncOutputQ.flush();
-        }
-    }
-
-    @Override
-    public void enableCallback() {
-        // not used
-        assert false;
+        writePublisher.flushAsync();
     }
 
-    boolean asyncOutput(ByteBufferReference[] refs, AsyncWriteQueue delayCallback) {
-        try {
-            ByteBuffer[] bufs = ByteBufferReference.toBuffers(refs);
-            while (Utils.remaining(bufs) > 0) {
-                long n = chan.write(bufs);
-                if (n == 0) {
-                    delayCallback.setDelayed(refs);
-                    client.registerEvent(new WriteEvent());
-                    return false;
-                }
-            }
-            ByteBufferReference.clear(refs);
-        } catch (IOException e) {
-            shutdown();
-        }
-        return true;
-    }
 
     @Override
     public String toString() {
@@ -255,7 +179,7 @@
     }
 
     /**
-     * Close this connection
+     * Closes this connection
      */
     @Override
     public synchronized void close() {
@@ -264,80 +188,23 @@
         }
         closed = true;
         try {
-            Log.logError("Closing: " + toString());
+            Log.logTrace("Closing: " + toString());
             chan.close();
         } catch (IOException e) {}
     }
 
     @Override
     void shutdownInput() throws IOException {
+        debug.log(Level.DEBUG, "Shutting down input");
         chan.shutdownInput();
     }
 
     @Override
     void shutdownOutput() throws IOException {
+        debug.log(Level.DEBUG, "Shutting down output");
         chan.shutdownOutput();
     }
 
-    void shutdown() {
-        close();
-        errorReceiver.accept(new IOException("Connection aborted"));
-    }
-
-    void asyncRead() {
-        synchronized (reading) {
-            try {
-                while (asyncReading) {
-                    ByteBufferReference buf = readBufferSupplier.get();
-                    int n = chan.read(buf.get());
-                    if (n == -1) {
-                        throw new IOException();
-                    }
-                    if (n == 0) {
-                        buf.clear();
-                        return;
-                    }
-                    buf.get().flip();
-                    asyncReceiver.accept(buf);
-                }
-            } catch (IOException e) {
-                shutdown();
-            }
-        }
-    }
-
-    @Override
-    protected ByteBuffer readImpl() throws IOException {
-        ByteBuffer dst = ByteBuffer.allocate(8192);
-        int n = readImpl(dst);
-        if (n > 0) {
-            return dst;
-        } else if (n == 0) {
-            return Utils.EMPTY_BYTEBUFFER;
-        } else {
-            return null;
-        }
-    }
-
-    private int readImpl(ByteBuffer buf) throws IOException {
-        int mark = buf.position();
-        int n;
-        // FIXME: this hack works in conjunction with the corresponding change
-        // in jdk.incubator.http.RawChannel.registerEvent
-        //if ((n = buffer.remaining()) != 0) {
-            //buf.put(buffer);
-        //} else {
-            n = chan.read(buf);
-        //}
-        if (n == -1) {
-            return -1;
-        }
-        Utils.flipToMark(buf, mark);
-        // String s = "Receive (" + n + " bytes) ";
-        //debugPrint(s, buf);
-        return n;
-    }
-
     @Override
     ConnectionPool.CacheKey cacheKey() {
         return new ConnectionPool.CacheKey(address, null);
@@ -348,98 +215,6 @@
         return connected;
     }
 
-    // used for all output in HTTP/2
-    class WriteEvent extends AsyncEvent {
-        WriteEvent() {
-            super(0);
-        }
-
-        @Override
-        public SelectableChannel channel() {
-            return chan;
-        }
-
-        @Override
-        public int interestOps() {
-            return SelectionKey.OP_WRITE;
-        }
-
-        @Override
-        public void handle() {
-            flushRegistered();
-        }
-
-        @Override
-        public void abort() {
-            shutdown();
-        }
-    }
-
-    // used for all input in HTTP/2
-    class ReadEvent extends AsyncEvent {
-        ReadEvent() {
-            super(AsyncEvent.REPEATING); // && !BLOCKING
-        }
-
-        @Override
-        public SelectableChannel channel() {
-            return chan;
-        }
-
-        @Override
-        public int interestOps() {
-            return SelectionKey.OP_READ;
-        }
-
-        @Override
-        public void handle() {
-            asyncRead();
-        }
-
-        @Override
-        public void abort() {
-            shutdown();
-        }
-
-        @Override
-        public String toString() {
-            return super.toString() + "/" + chan;
-        }
-    }
-
-    // used in blocking channels only
-    class ReceiveResponseEvent extends AsyncEvent {
-        CompletableFuture<Void> cf;
-
-        ReceiveResponseEvent(CompletableFuture<Void> cf) {
-            super(AsyncEvent.BLOCKING);
-            this.cf = cf;
-        }
-        @Override
-        public SelectableChannel channel() {
-            return chan;
-        }
-
-        @Override
-        public void handle() {
-            cf.complete(null);
-        }
-
-        @Override
-        public int interestOps() {
-            return SelectionKey.OP_READ;
-        }
-
-        @Override
-        public void abort() {
-            close();
-        }
-
-        @Override
-        public String toString() {
-            return super.toString() + "/" + chan;
-        }
-    }
 
     @Override
     boolean isSecure() {
@@ -451,24 +226,91 @@
         return false;
     }
 
-    @Override
-    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-                                  Consumer<Throwable> errorReceiver,
-                                  Supplier<ByteBufferReference> readBufferSupplier) {
-        this.asyncReceiver = asyncReceiver;
-        this.errorReceiver = errorReceiver;
-        this.readBufferSupplier = readBufferSupplier;
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+    private static final class PlainDetachedChannel
+            extends DetachedConnectionChannel {
+        final PlainHttpConnection plainConnection;
+        boolean closed;
+        PlainDetachedChannel(PlainHttpConnection conn) {
+            // We're handing the connection channel over to a web socket.
+            // We need the selector manager's thread to stay alive until
+            // the WebSocket is closed.
+            conn.client().webSocketOpen();
+            this.plainConnection = conn;
+        }
+
+        @Override
+        SocketChannel channel() {
+            return plainConnection.channel();
+        }
+
+        @Override
+        ByteBuffer read() throws IOException {
+            ByteBuffer dst = ByteBuffer.allocate(8192);
+            int n = readImpl(dst);
+            if (n > 0) {
+                return dst;
+            } else if (n == 0) {
+                return Utils.EMPTY_BYTEBUFFER;
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public void close() {
+            HttpClientImpl client = plainConnection.client();
+            try {
+                plainConnection.close();
+            } finally {
+                // notify the HttpClientImpl that the websocket is no
+                // no longer operating.
+                synchronized(this) {
+                    if (closed == true) return;
+                    closed = true;
+                }
+                client.webSocketClose();
+            }
+        }
+
+        @Override
+        public long write(ByteBuffer[] buffers, int start, int number)
+                throws IOException
+        {
+            return channel().write(buffers, start, number);
+        }
+
+        @Override
+        public void shutdownInput() throws IOException {
+            plainConnection.shutdownInput();
+        }
+
+        @Override
+        public void shutdownOutput() throws IOException {
+            plainConnection.shutdownOutput();
+        }
+
+        private int readImpl(ByteBuffer buf) throws IOException {
+            int mark = buf.position();
+            int n;
+            n = channel().read(buf);
+            if (n == -1) {
+                return -1;
+            }
+            Utils.flipToMark(buf, mark);
+            return n;
+        }
     }
 
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
     @Override
-    CompletableFuture<Void> whenReceivingResponse() {
-        CompletableFuture<Void> cf = new MinimalFuture<>();
-        try {
-            ReceiveResponseEvent evt = new ReceiveResponseEvent(cf);
-            client.registerEvent(evt);
-        } catch (IOException e) {
-            cf.completeExceptionally(e);
-        }
-        return cf;
+    DetachedConnectionChannel detachChannel() {
+        client().cancelRegistration(channel());
+        return new PlainDetachedChannel(this);
     }
+
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,71 +25,28 @@
 
 package jdk.incubator.http;
 
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.HttpResponse.BodyHandler;
-
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
+import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 
 /**
  * A plain text socket tunnel through a proxy. Uses "CONNECT" but does not
  * encrypt. Used by WebSocket, as well as HTTP over SSL + Proxy.
  * Wrapped in SSLTunnelConnection or AsyncSSLTunnelConnection for encryption.
  */
-class PlainTunnelingConnection extends HttpConnection implements AsyncConnection {
+final class PlainTunnelingConnection extends HttpConnection {
 
     final PlainHttpConnection delegate;
     protected final InetSocketAddress proxyAddr;
     private volatile boolean connected;
 
-    @Override
-    public CompletableFuture<Void> connectAsync() {
-        return delegate.connectAsync()
-            .thenCompose((Void v) -> {
-                HttpRequestImpl req = new HttpRequestImpl("CONNECT", client, address);
-                MultiExchange<Void,Void> mconnectExchange = new MultiExchange<>(req, client, this::ignore);
-                return mconnectExchange.responseAsync()
-                    .thenCompose((HttpResponseImpl<Void> resp) -> {
-                        CompletableFuture<Void> cf = new MinimalFuture<>();
-                        if (resp.statusCode() != 200) {
-                            cf.completeExceptionally(new IOException("Tunnel failed"));
-                        } else {
-                            connected = true;
-                            cf.complete(null);
-                        }
-                        return cf;
-                    });
-            });
-    }
-
-    private HttpResponse.BodyProcessor<Void> ignore(int status, HttpHeaders hdrs) {
-        return HttpResponse.BodyProcessor.discard((Void)null);
-    }
-
-    @Override
-    public void connect() throws IOException, InterruptedException {
-        delegate.connect();
-        HttpRequestImpl req = new HttpRequestImpl("CONNECT", client, address);
-        MultiExchange<Void,Void> mul = new MultiExchange<>(req, client, BodyHandler.<Void>discard(null));
-        Exchange<Void> connectExchange = new Exchange<>(req, mul);
-        Response r = connectExchange.responseImpl(delegate);
-        if (r.statusCode() != 200) {
-            throw new IOException("Tunnel failed");
-        }
-        connected = true;
-    }
-
-    @Override
-    boolean connected() {
-        return connected;
-    }
-
     protected PlainTunnelingConnection(InetSocketAddress addr,
                                        InetSocketAddress proxy,
                                        HttpClientImpl client) {
@@ -99,26 +56,62 @@
     }
 
     @Override
+    public CompletableFuture<Void> connectAsync() {
+        debug.log(Level.DEBUG, "Connecting plain connection");
+        return delegate.connectAsync()
+            .thenCompose((Void v) -> {
+                debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
+                HttpClientImpl client = client();
+                assert client != null;
+                HttpRequestImpl req = new HttpRequestImpl("CONNECT", address);
+                MultiExchange<Void,Void> mulEx = new MultiExchange<>(req, client, discard(null), null);
+                Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
+
+                return connectExchange
+                        .responseAsyncImpl(delegate)
+                        .thenCompose((Response resp) -> {
+                            CompletableFuture<Void> cf = new MinimalFuture<>();
+                            debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
+                            if (resp.statusCode() != 200) {
+                                cf.completeExceptionally(new IOException(
+                                        "Tunnel failed, got: "+ resp.statusCode()));
+                            } else {
+                                // get the initial/remaining bytes
+                                ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).getBuffer();
+                                int remaining = b.remaining();
+                                assert remaining == 0: "Unexpected remaining: " + remaining;
+                                connected = true;
+                                cf.complete(null);
+                            }
+                            return cf;
+                        });
+            });
+    }
+
+    @Override
+    HttpPublisher publisher() { return delegate.publisher(); }
+
+    @Override
+    boolean connected() {
+        return connected;
+    }
+
+    @Override
     SocketChannel channel() {
         return delegate.channel();
     }
 
     @Override
+    FlowTube getConnectionFlow() {
+        return delegate.getConnectionFlow();
+    }
+
+    @Override
     ConnectionPool.CacheKey cacheKey() {
         return new ConnectionPool.CacheKey(null, proxyAddr);
     }
 
     @Override
-    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-        return delegate.write(buffers, start, number);
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        return delegate.write(buffer);
-    }
-
-    @Override
     public void writeAsync(ByteBufferReference[] buffers) throws IOException {
         delegate.writeAsync(buffers);
     }
@@ -150,16 +143,6 @@
     }
 
     @Override
-    CompletableFuture<Void> whenReceivingResponse() {
-        return delegate.whenReceivingResponse();
-    }
-
-    @Override
-    protected ByteBuffer readImpl() throws IOException {
-        return delegate.readImpl();
-    }
-
-    @Override
     boolean isSecure() {
         return false;
     }
@@ -169,31 +152,11 @@
         return true;
     }
 
-    @Override
-    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
-            Consumer<Throwable> errorReceiver,
-            Supplier<ByteBufferReference> readBufferSupplier) {
-        delegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
-    }
-
-    @Override
-    public void startReading() {
-        delegate.startReading();
-    }
-
+    // Support for WebSocket/RawChannelImpl which unfortunately
+    // still depends on synchronous read/writes.
+    // It should be removed when RawChannelImpl moves to using asynchronous APIs.
     @Override
-    public void stopAsyncReading() {
-        delegate.stopAsyncReading();
-    }
-
-    @Override
-    public void enableCallback() {
-        delegate.enableCallback();
-    }
-
-    @Override
-    synchronized void configureMode(Mode mode) throws IOException {
-        super.configureMode(mode);
-        delegate.configureMode(mode);
+    DetachedConnectionChannel detachChannel() {
+        return delegate.detachChannel();
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PrivilegedExecutor.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2015, 2017, 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.incubator.http;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Executes tasks within a given access control context, and by a given executor.
+ */
+class PrivilegedExecutor implements Executor {
+
+    /** The underlying executor. May be provided by the user. */
+    final Executor executor;
+    /** The ACC to execute the tasks within. */
+    final AccessControlContext acc;
+
+    public PrivilegedExecutor(Executor executor, AccessControlContext acc) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(acc);
+        this.executor = executor;
+        this.acc = acc;
+    }
+
+    private static class PrivilegedRunnable implements Runnable {
+        private final Runnable r;
+        private final AccessControlContext acc;
+        PrivilegedRunnable(Runnable r, AccessControlContext acc) {
+            this.r = r;
+            this.acc = acc;
+        }
+        @Override
+        public void run() {
+            PrivilegedAction<Void> pa = () -> { r.run(); return null; };
+            AccessController.doPrivileged(pa, acc);
+        }
+    }
+
+    @Override
+    public void execute(Runnable r) {
+        executor.execute(new PrivilegedRunnable(r, acc));
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -27,10 +27,12 @@
 
 import java.util.Iterator;
 import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.SequentialScheduler;
 
 /**
- * A Publisher that is expected to run in same thread as subscriber.
- * Items are obtained from Iterable. Each new subscription gets a new Iterator.
+ * A Publisher that publishes items obtained from the given Iterable. Each new
+ * subscription gets a new Iterator.
  */
 class PullPublisher<T> implements Flow.Publisher<T> {
 
@@ -49,41 +51,54 @@
 
         private final Flow.Subscriber<? super T> subscriber;
         private final Iterator<T> iter;
-        private boolean done = false;
-        private long demand = 0;
-        private int recursion = 0;
+        private volatile boolean completed;
+        private volatile boolean cancelled;
+        private volatile Throwable error;
+        final SequentialScheduler pullScheduler = new SequentialScheduler(new PullTask());
+        private final Demand demand = new Demand();
 
         Subscription(Flow.Subscriber<? super T> subscriber, Iterator<T> iter) {
             this.subscriber = subscriber;
             this.iter = iter;
         }
 
-        @Override
-        public void request(long n) {
-            if (done) {
-                subscriber.onError(new IllegalArgumentException("request(" + n + ")"));
-            }
-            demand += n;
-            recursion ++;
-            if (recursion > 1) {
-                return;
-            }
-            while (demand > 0) {
-                done = !iter.hasNext();
-                if (done) {
-                    subscriber.onComplete();
-                    recursion --;
+        final class PullTask extends SequentialScheduler.CompleteRestartableTask {
+            @Override
+            protected void run() {
+                if (completed) {
+                    pullScheduler.stop();
                     return;
                 }
-                subscriber.onNext(iter.next());
-                demand --;
+                Throwable t = error;
+                if (t != null) {
+                    completed = true;
+                    pullScheduler.stop();
+                    subscriber.onError(t);
+                }
+                if (demand.tryDecrement()) {
+                    boolean done = completed = !iter.hasNext();
+                    if (done)
+                        subscriber.onComplete();
+                    else
+                        subscriber.onNext(iter.next());
+                }
             }
         }
 
         @Override
-        public void cancel() {
-            done = true;
+        public void request(long n) {
+            if (cancelled) {
+                error = new IllegalArgumentException("request("
+                                                      + n + "): cancelled");
+            } else {
+                demand.increase(n);
+            }
+            pullScheduler.runOrSchedule();
         }
 
+        @Override
+        public void cancel() {
+            cancelled = true;
+        }
     }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -25,7 +25,11 @@
 
 package jdk.incubator.http;
 
+import java.security.AccessControlContext;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
 import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Log;
 
@@ -38,59 +42,82 @@
     final CompletableFuture<Void> resultCF;
     final CompletableFuture<Void> noMorePushesCF;
 
-    volatile Throwable error; // any exception that occured during pushes
+    volatile Throwable error; // any exception that occurred during pushes
 
     // CF for main response
     final CompletableFuture<HttpResponse<T>> mainResponse;
 
-    // user's processor object
-    final HttpResponse.MultiProcessor<U, T> multiProcessor;
+    // user's subscriber object
+    final HttpResponse.MultiSubscriber<U, T> multiSubscriber;
 
     final HttpResponse.BodyHandler<T> mainBodyHandler;
 
+    private final AccessControlContext acc;
+
     int numberOfPushes;
     int remainingPushes;
     boolean noMorePushes = false;
 
-    PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor, HttpRequestImpl req) {
-        this(multiProcessor, req, new MinimalFuture<>());
+    PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
+              HttpRequestImpl req,
+              AccessControlContext acc) {
+        this(multiSubscriber, req, new MinimalFuture<>(), acc);
     }
 
     // Check mainBodyHandler before calling nested constructor.
-    private PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor,
-            HttpRequestImpl req,
-            CompletableFuture<HttpResponse<T>> mainResponse) {
-        this(multiProcessor, mainResponse,
-             multiProcessor.onRequest(req).orElseThrow(
-                    () -> new IllegalArgumentException(
-                     "A valid body processor for the main response is required")));
+    private PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
+                      HttpRequestImpl req,
+                      CompletableFuture<HttpResponse<T>> mainResponse,
+                      AccessControlContext acc) {
+        this(multiSubscriber,
+             mainResponse,
+             multiSubscriber.onRequest(req),
+             acc);
     }
 
-    // This private constructor is called after all parameters have been
-    // checked.
-    private PushGroup(HttpResponse.MultiProcessor<U, T> multiProcessor,
+    // This private constructor is called after all parameters have been checked.
+    private PushGroup(HttpResponse.MultiSubscriber<U, T> multiSubscriber,
                       CompletableFuture<HttpResponse<T>> mainResponse,
-                      HttpResponse.BodyHandler<T> mainBodyHandler) {
+                      HttpResponse.BodyHandler<T> mainBodyHandler,
+                      AccessControlContext acc) {
 
         assert mainResponse != null; // A new instance is created above
         assert mainBodyHandler != null; // should have been checked above
 
         this.resultCF = new MinimalFuture<>();
         this.noMorePushesCF = new MinimalFuture<>();
-        this.multiProcessor = multiProcessor;
+        this.multiSubscriber = multiSubscriber;
         this.mainResponse = mainResponse.thenApply(r -> {
-            multiProcessor.onResponse(r);
+            multiSubscriber.onResponse(r);
             return r;
         });
         this.mainBodyHandler = mainBodyHandler;
+        if (acc != null) {
+            // Restricts the file publisher with the senders ACC, if any
+            if (mainBodyHandler instanceof UntrustedBodyHandler)
+                ((UntrustedBodyHandler)this.mainBodyHandler).setAccessControlContext(acc);
+        }
+        this.acc = acc;
     }
 
     CompletableFuture<Void> groupResult() {
         return resultCF;
     }
 
-    HttpResponse.MultiProcessor<U, T> processor() {
-        return multiProcessor;
+    HttpResponse.MultiSubscriber<U, T> subscriber() {
+        return multiSubscriber;
+    }
+
+    Optional<BodyHandler<T>> handlerForPushRequest(HttpRequest ppRequest) {
+        Optional<BodyHandler<T>> bh = multiSubscriber.onPushPromise(ppRequest);
+        if (acc != null && bh.isPresent()) {
+            // Restricts the file publisher with the senders ACC, if any
+            BodyHandler<T> x = bh.get();
+            if (x instanceof UntrustedBodyHandler)
+                ((UntrustedBodyHandler)x).setAccessControlContext(acc);
+            bh = Optional.of(x);
+        }
+        return bh;
     }
 
     HttpResponse.BodyHandler<T> mainResponseHandler() {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -32,6 +32,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.SelectableChannel;
 import java.nio.channels.SocketChannel;
+import java.util.function.Supplier;
 
 /*
  * Each RawChannel corresponds to a TCP connection (SocketChannel) but is
@@ -41,17 +42,19 @@
 final class RawChannelImpl implements RawChannel {
 
     private final HttpClientImpl client;
-    private final HttpConnection connection;
+    private final HttpConnection.DetachedConnectionChannel detachedChannel;
     private final Object         initialLock = new Object();
-    private ByteBuffer           initial;
+    private Supplier<ByteBuffer> initial;
 
     RawChannelImpl(HttpClientImpl client,
                    HttpConnection connection,
-                   ByteBuffer initial)
+                   Supplier<ByteBuffer> initial)
             throws IOException
     {
         this.client = client;
-        this.connection = connection;
+        this.detachedChannel = connection.detachChannel();
+        this.initial = initial;
+
         SocketChannel chan = connection.channel();
         client.cancelRegistration(chan);
         // Constructing a RawChannel is supposed to have a "hand over"
@@ -64,15 +67,11 @@
                 chan.close();
             } catch (IOException e1) {
                 e.addSuppressed(e1);
+            } finally {
+                detachedChannel.close();
             }
             throw e;
         }
-        // empty the initial buffer into our own copy.
-        synchronized (initialLock) {
-            this.initial = initial.hasRemaining()
-                    ? Utils.copy(initial)
-                    : Utils.EMPTY_BYTEBUFFER;
-        }
     }
 
     private class NonBlockingRawAsyncEvent extends AsyncEvent {
@@ -80,13 +79,13 @@
         private final RawEvent re;
 
         NonBlockingRawAsyncEvent(RawEvent re) {
-            super(0); // !BLOCKING & !REPEATING
+            // !BLOCKING & !REPEATING
             this.re = re;
         }
 
         @Override
         public SelectableChannel channel() {
-            return connection.channel();
+            return detachedChannel.channel();
         }
 
         @Override
@@ -100,7 +99,7 @@
         }
 
         @Override
-        public void abort() { }
+        public void abort(IOException ioe) { }
     }
 
     @Override
@@ -110,8 +109,9 @@
 
     @Override
     public ByteBuffer read() throws IOException {
-        assert !connection.channel().isBlocking();
-        return connection.read();
+        assert !detachedChannel.channel().isBlocking();
+        // connection.read() will no longer be available.
+        return detachedChannel.read();
     }
 
     @Override
@@ -120,7 +120,9 @@
             if (initial == null) {
                 throw new IllegalStateException();
             }
-            ByteBuffer ref = initial;
+            ByteBuffer ref = initial.get();
+            ref = ref.hasRemaining() ? Utils.copy(ref)
+                    : Utils.EMPTY_BYTEBUFFER;
             initial = null;
             return ref;
         }
@@ -128,21 +130,29 @@
 
     @Override
     public long write(ByteBuffer[] src, int offset, int len) throws IOException {
-        return connection.write(src, offset, len);
+        // this makes the whitebox driver test fail.
+        return detachedChannel.write(src, offset, len);
     }
 
     @Override
     public void shutdownInput() throws IOException {
-        connection.shutdownInput();
+        detachedChannel.shutdownInput();
     }
 
     @Override
     public void shutdownOutput() throws IOException {
-        connection.shutdownOutput();
+        detachedChannel.shutdownOutput();
     }
 
     @Override
     public void close() throws IOException {
-        connection.close();
+        detachedChannel.close();
     }
+
+    @Override
+    public String toString() {
+        return super.toString()+"("+ detachedChannel.toString() + ")";
+    }
+
+
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestProcessors.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,312 +0,0 @@
-/*
- * Copyright (c) 2016, 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.incubator.http;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Flow;
-import java.util.function.Supplier;
-import jdk.incubator.http.internal.common.Utils;
-
-class RequestProcessors {
-
-    static class ByteArrayProcessor implements HttpRequest.BodyProcessor {
-        private volatile Flow.Publisher<ByteBuffer> delegate;
-        private final int length;
-        private final byte[] content;
-        private final int offset;
-
-        ByteArrayProcessor(byte[] content) {
-            this(content, 0, content.length);
-        }
-
-        ByteArrayProcessor(byte[] content, int offset, int length) {
-            this.content = content;
-            this.offset = offset;
-            this.length = length;
-        }
-
-        List<ByteBuffer> copy(byte[] content, int offset, int length) {
-            List<ByteBuffer> bufs = new ArrayList<>();
-            while (length > 0) {
-                ByteBuffer b = ByteBuffer.allocate(Math.min(Utils.BUFSIZE, length));
-                int max = b.capacity();
-                int tocopy = Math.min(max, length);
-                b.put(content, offset, tocopy);
-                offset += tocopy;
-                length -= tocopy;
-                b.flip();
-                bufs.add(b);
-            }
-            return bufs;
-        }
-
-        @Override
-        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
-            List<ByteBuffer> copy = copy(content, offset, length);
-            this.delegate = new PullPublisher<>(copy);
-            delegate.subscribe(subscriber);
-        }
-
-        @Override
-        public long contentLength() {
-            return length;
-        }
-    }
-
-    // This implementation has lots of room for improvement.
-    static class IterableProcessor implements HttpRequest.BodyProcessor {
-        private volatile Flow.Publisher<ByteBuffer> delegate;
-        private final Iterable<byte[]> content;
-        private volatile long contentLength;
-
-        IterableProcessor(Iterable<byte[]> content) {
-            this.content = content;
-        }
-
-        // The ByteBufferIterator will iterate over the byte[] arrays in
-        // the content one at the time.
-        //
-        class ByteBufferIterator implements Iterator<ByteBuffer> {
-            final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
-            final Iterator<byte[]> iterator = content.iterator();
-            @Override
-            public boolean hasNext() {
-                return !buffers.isEmpty() || iterator.hasNext();
-            }
-
-            @Override
-            public ByteBuffer next() {
-                ByteBuffer buffer = buffers.poll();
-                while (buffer == null) {
-                    copy();
-                    buffer = buffers.poll();
-                }
-                return buffer;
-            }
-
-            ByteBuffer getBuffer() {
-                return Utils.getBuffer();
-            }
-
-            void copy() {
-                byte[] bytes = iterator.next();
-                int length = bytes.length;
-                if (length == 0 && iterator.hasNext()) {
-                    // avoid inserting empty buffers, except
-                    // if that's the last.
-                    return;
-                }
-                int offset = 0;
-                do {
-                    ByteBuffer b = getBuffer();
-                    int max = b.capacity();
-
-                    int tocopy = Math.min(max, length);
-                    b.put(bytes, offset, tocopy);
-                    offset += tocopy;
-                    length -= tocopy;
-                    b.flip();
-                    buffers.add(b);
-                } while (length > 0);
-            }
-        }
-
-        public Iterator<ByteBuffer> iterator() {
-            return new ByteBufferIterator();
-        }
-
-        @Override
-        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
-            Iterable<ByteBuffer> iterable = this::iterator;
-            this.delegate = new PullPublisher<>(iterable);
-            delegate.subscribe(subscriber);
-        }
-
-        static long computeLength(Iterable<byte[]> bytes) {
-            long len = 0;
-            for (byte[] b : bytes) {
-                len = Math.addExact(len, (long)b.length);
-            }
-            return len;
-        }
-
-        @Override
-        public long contentLength() {
-            if (contentLength == 0) {
-                synchronized(this) {
-                    if (contentLength == 0) {
-                        contentLength = computeLength(content);
-                    }
-                }
-            }
-            return contentLength;
-        }
-    }
-
-    static class StringProcessor extends ByteArrayProcessor {
-        public StringProcessor(String content, Charset charset) {
-            super(content.getBytes(charset));
-        }
-    }
-
-    static class EmptyProcessor implements HttpRequest.BodyProcessor {
-        PseudoPublisher<ByteBuffer> delegate = new PseudoPublisher<>();
-
-        @Override
-        public long contentLength() {
-            return 0;
-        }
-
-        @Override
-        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
-            delegate.subscribe(subscriber);
-        }
-    }
-
-    static class FileProcessor extends InputStreamProcessor
-        implements HttpRequest.BodyProcessor
-    {
-        File file;
-
-        FileProcessor(Path name) {
-            super(() -> create(name));
-            file = name.toFile();
-        }
-
-        static FileInputStream create(Path name) {
-            try {
-                return new FileInputStream(name.toFile());
-            } catch (FileNotFoundException e) {
-                throw new UncheckedIOException(e);
-            }
-        }
-        @Override
-        public long contentLength() {
-            return file.length();
-        }
-    }
-
-    /**
-     * Reads one buffer ahead all the time, blocking in hasNext()
-     */
-    static class StreamIterator implements Iterator<ByteBuffer> {
-        final InputStream is;
-        ByteBuffer nextBuffer;
-        boolean need2Read = true;
-        boolean haveNext;
-        Throwable error;
-
-        StreamIterator(InputStream is) {
-            this.is = is;
-        }
-
-        Throwable error() {
-            return error;
-        }
-
-        private int read() {
-            nextBuffer = Utils.getBuffer();
-            nextBuffer.clear();
-            byte[] buf = nextBuffer.array();
-            int offset = nextBuffer.arrayOffset();
-            int cap = nextBuffer.capacity();
-            try {
-                int n = is.read(buf, offset, cap);
-                if (n == -1) {
-                    is.close();
-                    return -1;
-                }
-                //flip
-                nextBuffer.limit(n);
-                nextBuffer.position(0);
-                return n;
-            } catch (IOException ex) {
-                error = ex;
-                return -1;
-            }
-        }
-
-        @Override
-        public synchronized boolean hasNext() {
-            if (need2Read) {
-                haveNext = read() != -1;
-                if (haveNext) {
-                    need2Read = false;
-                }
-                return haveNext;
-            }
-            return haveNext;
-        }
-
-        @Override
-        public synchronized ByteBuffer next() {
-            if (!hasNext()) {
-                throw new NoSuchElementException();
-            }
-            need2Read = true;
-            return nextBuffer;
-        }
-
-    }
-
-    static class InputStreamProcessor implements HttpRequest.BodyProcessor {
-        private final Supplier<? extends InputStream> streamSupplier;
-        private Flow.Publisher<ByteBuffer> delegate;
-
-        InputStreamProcessor(Supplier<? extends InputStream> streamSupplier) {
-            this.streamSupplier = streamSupplier;
-        }
-
-        @Override
-        public synchronized void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
-
-            InputStream is = streamSupplier.get();
-            if (is == null) {
-                throw new UncheckedIOException(new IOException("no inputstream supplied"));
-            }
-            this.delegate = new PullPublisher<>(() -> new StreamIterator(is));
-            delegate.subscribe(subscriber);
-        }
-
-        @Override
-        public long contentLength() {
-            return -1;
-        }
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestPublishers.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,351 @@
+/*
+ * Copyright (c) 2016, 2017, 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.incubator.http;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.function.Supplier;
+import jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.Utils;
+
+class RequestPublishers {
+
+    static class ByteArrayPublisher implements HttpRequest.BodyPublisher {
+        private volatile Flow.Publisher<ByteBuffer> delegate;
+        private final int length;
+        private final byte[] content;
+        private final int offset;
+        private final int bufSize;
+
+        ByteArrayPublisher(byte[] content) {
+            this(content, 0, content.length);
+        }
+
+        ByteArrayPublisher(byte[] content, int offset, int length) {
+            this(content, offset, length, Utils.BUFSIZE);
+        }
+
+        /* bufSize exposed for testing purposes */
+        ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) {
+            this.content = content;
+            this.offset = offset;
+            this.length = length;
+            this.bufSize = bufSize;
+        }
+
+        List<ByteBuffer> copy(byte[] content, int offset, int length) {
+            List<ByteBuffer> bufs = new ArrayList<>();
+            while (length > 0) {
+                ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length));
+                int max = b.capacity();
+                int tocopy = Math.min(max, length);
+                b.put(content, offset, tocopy);
+                offset += tocopy;
+                length -= tocopy;
+                b.flip();
+                bufs.add(b);
+            }
+            return bufs;
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+            List<ByteBuffer> copy = copy(content, offset, length);
+            this.delegate = new PullPublisher<>(copy);
+            delegate.subscribe(subscriber);
+        }
+
+        @Override
+        public long contentLength() {
+            return length;
+        }
+    }
+
+    // This implementation has lots of room for improvement.
+    static class IterablePublisher implements HttpRequest.BodyPublisher {
+        private volatile Flow.Publisher<ByteBuffer> delegate;
+        private final Iterable<byte[]> content;
+        private volatile long contentLength;
+
+        IterablePublisher(Iterable<byte[]> content) {
+            this.content = content;
+        }
+
+        // The ByteBufferIterator will iterate over the byte[] arrays in
+        // the content one at the time.
+        //
+        class ByteBufferIterator implements Iterator<ByteBuffer> {
+            final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
+            final Iterator<byte[]> iterator = content.iterator();
+            @Override
+            public boolean hasNext() {
+                return !buffers.isEmpty() || iterator.hasNext();
+            }
+
+            @Override
+            public ByteBuffer next() {
+                ByteBuffer buffer = buffers.poll();
+                while (buffer == null) {
+                    copy();
+                    buffer = buffers.poll();
+                }
+                return buffer;
+            }
+
+            ByteBuffer getBuffer() {
+                return Utils.getBuffer();
+            }
+
+            void copy() {
+                byte[] bytes = iterator.next();
+                int length = bytes.length;
+                if (length == 0 && iterator.hasNext()) {
+                    // avoid inserting empty buffers, except
+                    // if that's the last.
+                    return;
+                }
+                int offset = 0;
+                do {
+                    ByteBuffer b = getBuffer();
+                    int max = b.capacity();
+
+                    int tocopy = Math.min(max, length);
+                    b.put(bytes, offset, tocopy);
+                    offset += tocopy;
+                    length -= tocopy;
+                    b.flip();
+                    buffers.add(b);
+                } while (length > 0);
+            }
+        }
+
+        public Iterator<ByteBuffer> iterator() {
+            return new ByteBufferIterator();
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+            Iterable<ByteBuffer> iterable = this::iterator;
+            this.delegate = new PullPublisher<>(iterable);
+            delegate.subscribe(subscriber);
+        }
+
+        static long computeLength(Iterable<byte[]> bytes) {
+            long len = 0;
+            for (byte[] b : bytes) {
+                len = Math.addExact(len, (long)b.length);
+            }
+            return len;
+        }
+
+        @Override
+        public long contentLength() {
+            if (contentLength == 0) {
+                synchronized(this) {
+                    if (contentLength == 0) {
+                        contentLength = computeLength(content);
+                    }
+                }
+            }
+            return contentLength;
+        }
+    }
+
+    static class StringPublisher extends ByteArrayPublisher {
+        public StringPublisher(String content, Charset charset) {
+            super(content.getBytes(charset));
+        }
+    }
+
+    static class EmptyPublisher implements HttpRequest.BodyPublisher {
+        private final PseudoPublisher<ByteBuffer> delegate = new PseudoPublisher<>();
+
+        @Override
+        public long contentLength() {
+            return 0;
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+            delegate.subscribe(subscriber);
+        }
+    }
+
+    static class FilePublisher implements BodyPublisher  {
+        private final File file;
+        private volatile AccessControlContext acc;
+
+        FilePublisher(Path name) {
+            file = name.toFile();
+        }
+
+        void setAccessControlContext(AccessControlContext acc) {
+            this.acc = acc;
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+            if (System.getSecurityManager() != null && acc == null)
+                throw new InternalError(
+                        "Unexpected null acc when security manager has been installed");
+
+            InputStream is;
+            try {
+                PrivilegedExceptionAction<FileInputStream> pa =
+                        () -> new FileInputStream(file);
+                is = AccessController.doPrivileged(pa, acc);
+            } catch (PrivilegedActionException pae) {
+                throw new UncheckedIOException((IOException)pae.getCause());
+            }
+            PullPublisher<ByteBuffer> publisher =
+                    new PullPublisher<>(() -> new StreamIterator(is));
+            publisher.subscribe(subscriber);
+        }
+
+        @Override
+        public long contentLength() {
+            assert System.getSecurityManager() != null ? acc != null: true;
+            PrivilegedAction<Long> pa = () -> file.length();
+            return AccessController.doPrivileged(pa, acc);
+        }
+    }
+
+    /**
+     * Reads one buffer ahead all the time, blocking in hasNext()
+     */
+    static class StreamIterator implements Iterator<ByteBuffer> {
+        final InputStream is;
+        final Supplier<? extends ByteBuffer> bufSupplier;
+        volatile ByteBuffer nextBuffer;
+        volatile boolean need2Read = true;
+        volatile boolean haveNext;
+        volatile Throwable error;
+
+        StreamIterator(InputStream is) {
+            this(is, Utils::getBuffer);
+        }
+
+        StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) {
+            this.is = is;
+            this.bufSupplier = bufSupplier;
+        }
+
+        Throwable error() {
+            return error;
+        }
+
+        private int read() {
+            nextBuffer = bufSupplier.get();
+            nextBuffer.clear();
+            byte[] buf = nextBuffer.array();
+            int offset = nextBuffer.arrayOffset();
+            int cap = nextBuffer.capacity();
+            try {
+                int n = is.read(buf, offset, cap);
+                if (n == -1) {
+                    is.close();
+                    return -1;
+                }
+                //flip
+                nextBuffer.limit(n);
+                nextBuffer.position(0);
+                return n;
+            } catch (IOException ex) {
+                error = ex;
+                return -1;
+            }
+        }
+
+        @Override
+        public synchronized boolean hasNext() {
+            if (need2Read) {
+                haveNext = read() != -1;
+                if (haveNext) {
+                    need2Read = false;
+                }
+                return haveNext;
+            }
+            return haveNext;
+        }
+
+        @Override
+        public synchronized ByteBuffer next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            need2Read = true;
+            return nextBuffer;
+        }
+
+    }
+
+    static class InputStreamPublisher implements BodyPublisher {
+        private final Supplier<? extends InputStream> streamSupplier;
+
+        InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) {
+            this.streamSupplier = streamSupplier;
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+
+            InputStream is = streamSupplier.get();
+            if (is == null) {
+                throw new UncheckedIOException(new IOException("no inputstream supplied"));
+            }
+            PullPublisher<ByteBuffer> publisher =
+                    new PullPublisher<>(iterableOf(is));
+            publisher.subscribe(subscriber);
+        }
+
+        protected Iterable<ByteBuffer> iterableOf(InputStream is) {
+            return () -> new StreamIterator(is);
+        }
+
+        @Override
+        public long contentLength() {
+            return -1;
+        }
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,6 +25,8 @@
 
 package jdk.incubator.http;
 
+import java.net.URI;
+
 /**
  * Response headers and status code.
  */
@@ -66,4 +68,19 @@
     int statusCode() {
         return statusCode;
     }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        String method = request().method();
+        URI uri = request().uri();
+        String uristring = uri == null ? "" : uri.toString();
+        sb.append('(')
+          .append(method)
+          .append(" ")
+          .append(uristring)
+          .append(") ")
+          .append(statusCode());
+        return sb.toString();
+    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,8 +26,10 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.nio.ByteBuffer;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Consumer;
 import jdk.incubator.http.internal.common.Utils;
 
@@ -39,35 +41,27 @@
  */
 class ResponseContent {
 
-    final HttpResponse.BodyProcessor<?> pusher;
-    final HttpConnection connection;
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+
+    final HttpResponse.BodySubscriber<?> pusher;
     final int contentLength;
-    ByteBuffer buffer;
-    //ByteBuffer lastBufferUsed;
-    final ResponseHeaders headers;
-    private final Consumer<Optional<ByteBuffer>> dataConsumer;
-    private final Consumer<IOException> errorConsumer;
-    private final HttpClientImpl client;
+    final HttpHeaders headers;
     // this needs to run before we complete the body
     // so that connection can be returned to pool
     private final Runnable onFinished;
+    private final String dbgTag;
 
     ResponseContent(HttpConnection connection,
                     int contentLength,
-                    ResponseHeaders h,
-                    HttpResponse.BodyProcessor<?> userProcessor,
-                    Consumer<Optional<ByteBuffer>> dataConsumer,
-                    Consumer<IOException> errorConsumer,
+                    HttpHeaders h,
+                    HttpResponse.BodySubscriber<?> userProcessor,
                     Runnable onFinished)
     {
-        this.pusher = (HttpResponse.BodyProcessor)userProcessor;
-        this.connection = connection;
+        this.pusher = userProcessor;
         this.contentLength = contentLength;
         this.headers = h;
-        this.dataConsumer = dataConsumer;
-        this.errorConsumer = errorConsumer;
-        this.client = connection.client;
         this.onFinished = onFinished;
+        this.dbgTag = connection.dbgString() + "/ResponseContent";
     }
 
     static final int LF = 10;
@@ -77,7 +71,7 @@
 
     boolean chunkedContent, chunkedContentInitialized;
 
-    private boolean contentChunked() throws IOException {
+    boolean contentChunked() throws IOException {
         if (chunkedContentInitialized) {
             return chunkedContent;
         }
@@ -98,182 +92,374 @@
         return chunkedContent;
     }
 
-    /**
-     * Entry point for pusher. b is an initial ByteBuffer that may
-     * have some data in it. When this method returns, the body
-     * has been fully processed.
-     */
-    void pushBody(ByteBuffer b) {
-        try {
-            // TODO: check status
-            if (contentChunked()) {
-                pushBodyChunked(b);
-            } else {
-                pushBodyFixed(b);
-            }
-        } catch (IOException t) {
-            errorConsumer.accept(t);
+    interface BodyParser extends Consumer<ByteBuffer> {
+        void onSubscribe(AbstractSubscription sub);
+    }
+
+    // Returns a parser that will take care of parsing the received byte
+    // buffers and forward them to the BodyProcessor.
+    // When the parser is done, it will call onComplete.
+    // If parsing was successful, the throwable parameter will be null.
+    // Otherwise it will be the exception that occurred
+    // Note: revisit: it might be better to use a CompletableFuture than
+    //       a completion handler.
+    BodyParser getBodyParser(Consumer<Throwable> onComplete)
+        throws IOException {
+        if (contentChunked()) {
+            return new ChunkedBodyParser(onComplete);
+        } else {
+            return new FixedLengthBodyParser(contentLength, onComplete);
         }
     }
 
-    // reads and returns chunklen. Position of chunkbuf is first byte
-    // of chunk on return. chunklen includes the CR LF at end of chunk
-    int readChunkLen() throws IOException {
-        chunklen = 0;
-        boolean cr = false;
-        while (true) {
-            getHunk();
-            int c = chunkbuf.get();
-            if (cr) {
-                if (c == LF) {
-                    return chunklen + 2;
-                } else {
-                    throw new IOException("invalid chunk header");
-                }
-            }
-            if (c == CR) {
-                cr = true;
-            } else {
-                int digit = toDigit(c);
-                chunklen = chunklen * 16 + digit;
-            }
+
+    static enum ChunkState {READING_LENGTH, READING_DATA, DONE};
+    class ChunkedBodyParser implements BodyParser {
+        final ByteBuffer READMORE = Utils.EMPTY_BYTEBUFFER;
+        final Consumer<Throwable> onComplete;
+        final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+        final String dbgTag = ResponseContent.this.dbgTag + "/ChunkedBodyParser";
+
+        volatile Throwable closedExceptionally;
+        volatile int partialChunklen = 0; // partially read chunk len
+        volatile int chunklen = -1;  // number of bytes in chunk
+        volatile int bytesremaining;  // number of bytes in chunk left to be read incl CRLF
+        volatile boolean cr = false;  // tryReadChunkLength has found CR
+        volatile int bytesToConsume;  // number of bytes that still need to be consumed before proceeding
+        volatile ChunkState state = ChunkState.READING_LENGTH; // current state
+        volatile AbstractSubscription sub;
+        ChunkedBodyParser(Consumer<Throwable> onComplete) {
+            this.onComplete = onComplete;
         }
-    }
+
+        String dbgString() {
+            return dbgTag;
+        }
 
-    int chunklen = -1;      // number of bytes in chunk (fixed)
-    int bytesremaining;     // number of bytes in chunk left to be read incl CRLF
-    int bytesread;
-    ByteBuffer chunkbuf;    // initialise
+        @Override
+        public void onSubscribe(AbstractSubscription sub) {
+            debug.log(Level.DEBUG, () ->  "onSubscribe: "
+                        + pusher.getClass().getName());
+            pusher.onSubscribe(this.sub = sub);
+        }
 
-    // make sure we have at least 1 byte to look at
-    private void getHunk() throws IOException {
-        if (chunkbuf == null || !chunkbuf.hasRemaining()) {
-            chunkbuf = connection.read();
-        }
-    }
-
-    private void consumeBytes(int n) throws IOException {
-        getHunk();
-        while (n > 0) {
-            int e = Math.min(chunkbuf.remaining(), n);
-            chunkbuf.position(chunkbuf.position() + e);
-            n -= e;
-            if (n > 0) {
-                getHunk();
+        @Override
+        public void accept(ByteBuffer b) {
+            if (closedExceptionally != null) {
+                debug.log(Level.DEBUG, () ->  "already closed: "
+                            + closedExceptionally);
+                return;
             }
-        }
-    }
+            boolean completed = false;
+            try {
+                List<ByteBuffer> out = new ArrayList<>();
+                do {
+                    if (tryPushOneHunk(b, out))  {
+                        // We're done! (true if the final chunk was parsed).
+                        if (!out.isEmpty()) {
+                            // push what we have and complete
+                            // only reduce demand if we actually push something.
+                            // we would not have come here if there was no
+                            // demand.
+                            boolean hasDemand = sub.demand().tryDecrement();
+                            assert hasDemand;
+                            pusher.onNext(out);
+                        }
+                        debug.log(Level.DEBUG, () ->  "done!");
+                        assert closedExceptionally == null;
+                        assert state == ChunkState.DONE;
+                        onFinished.run();
+                        pusher.onComplete();
+                        completed = true;
+                        onComplete.accept(closedExceptionally); // should be null
+                        break;
+                    }
+                    // the buffer may contain several hunks, and therefore
+                    // we must loop while it's not exhausted.
+                } while (b.hasRemaining());
 
-    /**
-     * Returns a ByteBuffer containing a chunk of data or a "hunk" of data
-     * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
-     * ByteBuffer returned is obtained from response processor.
-     */
-    ByteBuffer readChunkedBuffer() throws IOException {
-        if (chunklen == -1) {
-            // new chunk
-            chunklen = readChunkLen() - 2;
-            bytesremaining =  chunklen;
-            if (chunklen == 0) {
-                consumeBytes(2);
-                return null;
+                if (!completed && !out.isEmpty()) {
+                    // push what we have.
+                    // only reduce demand if we actually push something.
+                    // we would not have come here if there was no
+                    // demand.
+                    boolean hasDemand = sub.demand().tryDecrement();
+                    assert hasDemand;
+                    pusher.onNext(out);
+                }
+                assert state == ChunkState.DONE || !b.hasRemaining();
+            } catch(Throwable t) {
+                closedExceptionally = t;
+                if (!completed) onComplete.accept(t);
             }
         }
 
-        getHunk();
-        bytesread = chunkbuf.remaining();
-        ByteBuffer returnBuffer = Utils.getBuffer();
-        int space = returnBuffer.remaining();
+        // reads and returns chunklen. Position of chunkbuf is first byte
+        // of chunk on return. chunklen includes the CR LF at end of chunk
+        // returns -1 if needs more bytes
+        private int tryReadChunkLen(ByteBuffer chunkbuf) throws IOException {
+            assert state == ChunkState.READING_LENGTH;
+            while (chunkbuf.hasRemaining()) {
+                int c = chunkbuf.get();
+                if (cr) {
+                    if (c == LF) {
+                        return partialChunklen;
+                    } else {
+                        throw new IOException("invalid chunk header");
+                    }
+                }
+                if (c == CR) {
+                    cr = true;
+                } else {
+                    int digit = toDigit(c);
+                    partialChunklen = partialChunklen * 16 + digit;
+                }
+            }
+            return -1;
+        }
+
+
+        // try to consume as many bytes as specified by bytesToConsume.
+        // returns the number of bytes that still need to be consumed.
+        // In practice this method is only called to consume one CRLF pair
+        // with bytesToConsume set to 2, so it will only return 0 (if completed),
+        // 1, or 2 (if chunkbuf doesn't have the 2 chars).
+        private int tryConsumeBytes(ByteBuffer chunkbuf) throws IOException {
+            int n = bytesToConsume;
+            if (n > 0) {
+                int e = Math.min(chunkbuf.remaining(), n);
+
+                // verifies some assertions
+                // this methods is called only to consume CRLF
+                if (Utils.ASSERTIONSENABLED) {
+                    assert n <= 2 && e <= 2;
+                    ByteBuffer tmp = chunkbuf.slice();
+                    // if n == 2 assert that we will first consume CR
+                    assert (n == 2 && e > 0) ? tmp.get() == CR : true;
+                    // if n == 1 || n == 2 && e == 2 assert that we then consume LF
+                    assert (n == 1 || e == 2) ? tmp.get() == LF : true;
+                }
+
+                chunkbuf.position(chunkbuf.position() + e);
+                n -= e;
+                bytesToConsume = n;
+            }
+            assert n >= 0;
+            return n;
+        }
+
+        /**
+         * Returns a ByteBuffer containing chunk of data or a "hunk" of data
+         * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
+         * If the given chunk does not have enough data this method return
+         * an empty ByteBuffer (READMORE).
+         * If we encounter the final chunk (an empty chunk) this method
+         * returns null.
+         */
+        ByteBuffer tryReadOneHunk(ByteBuffer chunk) throws IOException {
+            int unfulfilled = bytesremaining;
+            int toconsume = bytesToConsume;
+            ChunkState st = state;
+            if (st == ChunkState.READING_LENGTH && chunklen == -1) {
+                debug.log(Level.DEBUG, () ->  "Trying to read chunk len"
+                        + " (remaining in buffer:"+chunk.remaining()+")");
+                int clen = chunklen = tryReadChunkLen(chunk);
+                if (clen == -1) return READMORE;
+                debug.log(Level.DEBUG, "Got chunk len %d", clen);
+                cr = false; partialChunklen = 0;
+                unfulfilled = bytesremaining =  clen;
+                if (clen == 0) toconsume = bytesToConsume = 2; // that was the last chunk
+                else st = state = ChunkState.READING_DATA; // read the data
+            }
+
+            if (toconsume > 0) {
+                debug.log(Level.DEBUG,
+                        "Trying to consume bytes: %d (remaining in buffer: %s)",
+                        toconsume, chunk.remaining());
+                if (tryConsumeBytes(chunk) > 0) {
+                    return READMORE;
+                }
+            }
+
+            toconsume = bytesToConsume;
+            assert toconsume == 0;
+
 
-        int bytes2Copy = Math.min(bytesread, Math.min(bytesremaining, space));
-        Utils.copy(chunkbuf, returnBuffer, bytes2Copy);
-        returnBuffer.flip();
-        bytesremaining -= bytes2Copy;
-        if (bytesremaining == 0) {
-            consumeBytes(2);
-            chunklen = -1;
+            if (st == ChunkState.READING_LENGTH) {
+                // we will come here only if chunklen was 0, after having
+                // consumed the trailing CRLF
+                int clen = chunklen;
+                assert clen == 0;
+                debug.log(Level.DEBUG, "No more chunks: %d", clen);
+                // the DONE state is not really needed but it helps with
+                // assertions...
+                state = ChunkState.DONE;
+                return null;
+            }
+
+            int clen = chunklen;
+            assert clen > 0;
+            assert st == ChunkState.READING_DATA;
+
+            ByteBuffer returnBuffer = READMORE; // May be a hunk or a chunk
+            if (unfulfilled > 0) {
+                int bytesread = chunk.remaining();
+                debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
+                          bytesread, unfulfilled);
+
+                int bytes2return = Math.min(bytesread, unfulfilled);
+                debug.log(Level.DEBUG,  "Returning chunk bytes: %d", bytes2return);
+                returnBuffer = Utils.slice(chunk, bytes2return);
+                unfulfilled = bytesremaining -= bytes2return;
+                if (unfulfilled == 0) bytesToConsume = 2;
+            }
+
+            assert unfulfilled >= 0;
+
+            if (unfulfilled == 0) {
+                debug.log(Level.DEBUG,
+                        "No more bytes to read - %d yet to consume.",
+                        unfulfilled);
+                // check whether the trailing CRLF is consumed, try to
+                // consume it if not. If tryConsumeBytes needs more bytes
+                // then we will come back here later - skipping the block
+                // that reads data because remaining==0, and finding
+                // that the two bytes are now consumed.
+                if (tryConsumeBytes(chunk) == 0) {
+                    // we're done for this chunk! reset all states and
+                    // prepare to read the next chunk.
+                    chunklen = -1;
+                    partialChunklen = 0;
+                    cr = false;
+                    state = ChunkState.READING_LENGTH;
+                    debug.log(Level.DEBUG, "Ready to read next chunk");
+                }
+            }
+            if (returnBuffer == READMORE) {
+                debug.log(Level.DEBUG, "Need more data");
+            }
+            return returnBuffer;
         }
-        return returnBuffer;
+
+
+        // Attempt to parse and push one hunk from the buffer.
+        // Returns true if the final chunk was parsed.
+        // Returns false if we need to push more chunks.
+        private boolean tryPushOneHunk(ByteBuffer b, List<ByteBuffer> out)
+                throws IOException {
+            assert state != ChunkState.DONE;
+            ByteBuffer b1 = tryReadOneHunk(b);
+            if (b1 != null) {
+                //assert b1.hasRemaining() || b1 == READMORE;
+                if (b1.hasRemaining()) {
+                    debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
+                              b1.remaining());
+                    out.add(b1);
+                    debug.log(Level.DEBUG, "Chunk sent.");
+                }
+                return false; // we haven't parsed the final chunk yet.
+            } else {
+                return true; // we're done! the final chunk was parsed.
+            }
+        }
+
+        private int toDigit(int b) throws IOException {
+            if (b >= 0x30 && b <= 0x39) {
+                return b - 0x30;
+            }
+            if (b >= 0x41 && b <= 0x46) {
+                return b - 0x41 + 10;
+            }
+            if (b >= 0x61 && b <= 0x66) {
+                return b - 0x61 + 10;
+            }
+            throw new IOException("Invalid chunk header byte " + b);
+        }
+
     }
 
-    ByteBuffer initialBuffer;
-    int fixedBytesReturned;
+    class FixedLengthBodyParser implements BodyParser {
+        final int contentLength;
+        final Consumer<Throwable> onComplete;
+        final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+        final String dbgTag = ResponseContent.this.dbgTag + "/FixedLengthBodyParser";
+        volatile int remaining;
+        volatile Throwable closedExceptionally;
+        volatile AbstractSubscription sub;
+        FixedLengthBodyParser(int contentLength, Consumer<Throwable> onComplete) {
+            this.contentLength = this.remaining = contentLength;
+            this.onComplete = onComplete;
+        }
+
+        String dbgString() {
+            return dbgTag;
+        }
 
-    //ByteBuffer getResidue() {
-        //return lastBufferUsed;
-    //}
-
-    private void compactBuffer(ByteBuffer buf) {
-        buf.compact()
-           .flip();
-    }
+        @Override
+        public void onSubscribe(AbstractSubscription sub) {
+            debug.log(Level.DEBUG, () -> "length="
+                        + contentLength +", onSubscribe: "
+                        + pusher.getClass().getName());
+            pusher.onSubscribe(this.sub = sub);
+            try {
+                if (contentLength == 0) {
+                    pusher.onComplete();
+                    onComplete.accept(null);
+                }
+            } catch (Throwable t) {
+                closedExceptionally = t;
+                try {
+                    pusher.onError(t);
+                } finally {
+                    onComplete.accept(t);
+                }
+            }
+        }
 
-    /**
-     * Copies inbuf (numBytes from its position) to new buffer. The returned
-     * buffer's position is zero and limit is at end (numBytes)
-     */
-    private ByteBuffer copyBuffer(ByteBuffer inbuf, int numBytes) {
-        ByteBuffer b1 = Utils.getBuffer();
-        assert b1.remaining() >= numBytes;
-        byte[] b = b1.array();
-        inbuf.get(b, 0, numBytes);
-        b1.limit(numBytes);
-        return b1;
-    }
+        @Override
+        public void accept(ByteBuffer b) {
+            if (closedExceptionally != null) {
+                debug.log(Level.DEBUG, () -> "already closed: "
+                            + closedExceptionally);
+                return;
+            }
+            boolean completed = false;
+            try {
+                int unfulfilled = remaining;
+                debug.log(Level.DEBUG, "Parser got %d bytes (%d remaining / %d)",
+                        b.remaining(), unfulfilled, contentLength);
+                assert unfulfilled != 0 || contentLength == 0 || b.remaining() == 0;
+
+                if (unfulfilled == 0 && contentLength > 0) return;
 
-    private void pushBodyChunked(ByteBuffer b) throws IOException {
-        chunkbuf = b;
-        while (true) {
-            ByteBuffer b1 = readChunkedBuffer();
-            if (b1 != null) {
-                if (b1.hasRemaining()) {
-                    dataConsumer.accept(Optional.of(b1));
+                if (b.hasRemaining() && unfulfilled > 0) {
+                    // only reduce demand if we actually push something.
+                    // we would not have come here if there was no
+                    // demand.
+                    boolean hasDemand = sub.demand().tryDecrement();
+                    assert hasDemand;
+                    int amount = Math.min(b.remaining(), unfulfilled);
+                    unfulfilled = remaining -= amount;
+                    ByteBuffer buffer = Utils.slice(b, amount);
+                    pusher.onNext(List.of(buffer));
                 }
-            } else {
-                onFinished.run();
-                dataConsumer.accept(Optional.empty());
-                return;
+                if (unfulfilled == 0) {
+                    // We're done! All data has been received.
+                    assert closedExceptionally == null;
+                    onFinished.run();
+                    pusher.onComplete();
+                    completed = true;
+                    onComplete.accept(closedExceptionally); // should be null
+                } else {
+                    assert b.remaining() == 0;
+                }
+            } catch (Throwable t) {
+                debug.log(Level.DEBUG, "Unexpected exception", t);
+                closedExceptionally = t;
+                if (!completed) {
+                    onComplete.accept(t);
+                }
             }
         }
     }
-
-    private int toDigit(int b) throws IOException {
-        if (b >= 0x30 && b <= 0x39) {
-            return b - 0x30;
-        }
-        if (b >= 0x41 && b <= 0x46) {
-            return b - 0x41 + 10;
-        }
-        if (b >= 0x61 && b <= 0x66) {
-            return b - 0x61 + 10;
-        }
-        throw new IOException("Invalid chunk header byte " + b);
-    }
-
-    private void pushBodyFixed(ByteBuffer b) throws IOException {
-        int remaining = contentLength;
-        while (b.hasRemaining() && remaining > 0) {
-            ByteBuffer buffer = Utils.getBuffer();
-            int amount = Math.min(b.remaining(), remaining);
-            Utils.copy(b, buffer, amount);
-            remaining -= amount;
-            buffer.flip();
-            dataConsumer.accept(Optional.of(buffer));
-        }
-        while (remaining > 0) {
-            ByteBuffer buffer = connection.read();
-            if (buffer == null)
-                throw new IOException("connection closed");
-
-            int bytesread = buffer.remaining();
-            // assume for now that pipelining not implemented
-            if (bytesread > remaining) {
-                throw new IOException("too many bytes read");
-            }
-            remaining -= bytesread;
-            dataConsumer.accept(Optional.of(buffer));
-        }
-        onFinished.run();
-        dataConsumer.accept(Optional.empty());
-    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java	Sun Nov 05 17:32:13 2017 +0000
@@ -44,11 +44,14 @@
 import static jdk.incubator.http.internal.common.Utils.isValidValue;
 import static java.util.Objects.requireNonNull;
 
+
+// ####: Remove. Replaced with Http1HeaderDecoder
+
 /*
  * Reads entire header block off channel, in blocking mode.
  * This class is not thread-safe.
  */
-final class ResponseHeaders implements HttpHeaders {
+final class ResponseHeaders extends HttpHeaders {
 
     private static final char CR = '\r';
     private static final char LF = '\n';
@@ -63,27 +66,33 @@
      * leftovers (i.e. data, if any, beyond the header block) are accessible
      * from this same buffer from its position to its limit.
      */
-    ResponseHeaders(HttpConnection connection, ByteBuffer buffer) throws IOException {
-        requireNonNull(connection);
+    ResponseHeaders(BufferReader reader, ByteBuffer buffer) throws IOException {
+        requireNonNull(reader);
         requireNonNull(buffer);
-        InputStreamWrapper input = new InputStreamWrapper(connection, buffer);
+        InputStreamWrapper input =
+                new InputStreamWrapper(reader, buffer);
         delegate = ImmutableHeaders.of(parse(input));
     }
 
+    @FunctionalInterface
+    static interface BufferReader {
+        ByteBuffer read() throws IOException;
+    }
+
     static final class InputStreamWrapper extends InputStream {
-        final HttpConnection connection;
+        final BufferReader reader;
         ByteBuffer buffer;
         int lastRead = -1; // last byte read from the buffer
         int consumed = 0; // number of bytes consumed.
-        InputStreamWrapper(HttpConnection connection, ByteBuffer buffer) {
+        InputStreamWrapper(BufferReader reader, ByteBuffer buffer) {
             super();
-            this.connection = connection;
+            this.reader = reader;
             this.buffer = buffer;
         }
         @Override
         public int read() throws IOException {
             if (!buffer.hasRemaining()) {
-                buffer = connection.read();
+                buffer = reader.read();
                 if (buffer == null) {
                     return lastRead = -1;
                 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseProcessors.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,331 +0,0 @@
-/*
- * Copyright (c) 2016, 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.incubator.http;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Flow;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.common.Log;
-
-class ResponseProcessors {
-
-    static class ConsumerProcessor implements HttpResponse.BodyProcessor<Void> {
-        private final Consumer<Optional<byte[]>> consumer;
-        private Flow.Subscription subscription;
-        private final CompletableFuture<Void> result = new MinimalFuture<>();
-
-        ConsumerProcessor(Consumer<Optional<byte[]>> consumer) {
-            this.consumer = consumer;
-        }
-
-        @Override
-        public CompletionStage<Void> getBody() {
-            return result;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            this.subscription = subscription;
-            subscription.request(1);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            byte[] buf = new byte[item.remaining()];
-            item.get(buf);
-            consumer.accept(Optional.of(buf));
-            subscription.request(1);
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            result.completeExceptionally(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            consumer.accept(Optional.empty());
-            result.complete(null);
-        }
-
-    }
-
-    static class PathProcessor implements HttpResponse.BodyProcessor<Path> {
-
-        private final Path file;
-        private final CompletableFuture<Path> result = new MinimalFuture<>();
-
-        private Flow.Subscription subscription;
-        private FileChannel out;
-        private final OpenOption[] options;
-
-        PathProcessor(Path file, OpenOption... options) {
-            this.file = file;
-            this.options = options;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            this.subscription = subscription;
-            try {
-                out = FileChannel.open(file, options);
-            } catch (IOException e) {
-                result.completeExceptionally(e);
-                subscription.cancel();
-                return;
-            }
-            subscription.request(1);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            try {
-                out.write(item);
-            } catch (IOException ex) {
-                Utils.close(out);
-                subscription.cancel();
-                result.completeExceptionally(ex);
-            }
-            subscription.request(1);
-        }
-
-        @Override
-        public void onError(Throwable e) {
-            result.completeExceptionally(e);
-            Utils.close(out);
-        }
-
-        @Override
-        public void onComplete() {
-            Utils.close(out);
-            result.complete(file);
-        }
-
-        @Override
-        public CompletionStage<Path> getBody() {
-            return result;
-        }
-    }
-
-    static class ByteArrayProcessor<T> implements HttpResponse.BodyProcessor<T> {
-        private final Function<byte[], T> finisher;
-        private final CompletableFuture<T> result = new MinimalFuture<>();
-        private final List<ByteBuffer> received = new ArrayList<>();
-
-        private Flow.Subscription subscription;
-
-        ByteArrayProcessor(Function<byte[],T> finisher) {
-            this.finisher = finisher;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            if (this.subscription != null) {
-                subscription.cancel();
-                return;
-            }
-            this.subscription = subscription;
-            // We can handle whatever you've got
-            subscription.request(Long.MAX_VALUE);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            // incoming buffers are allocated by http client internally,
-            // and won't be used anywhere except this place.
-            // So it's free simply to store them for further processing.
-            if(item.hasRemaining()) {
-                received.add(item);
-            }
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            received.clear();
-            result.completeExceptionally(throwable);
-        }
-
-        static private byte[] join(List<ByteBuffer> bytes) {
-            int size = Utils.remaining(bytes);
-            byte[] res = new byte[size];
-            int from = 0;
-            for (ByteBuffer b : bytes) {
-                int l = b.remaining();
-                b.get(res, from, l);
-                from += l;
-            }
-            return res;
-        }
-
-        @Override
-        public void onComplete() {
-            try {
-                result.complete(finisher.apply(join(received)));
-                received.clear();
-            } catch (IllegalArgumentException e) {
-                result.completeExceptionally(e);
-            }
-        }
-
-        @Override
-        public CompletionStage<T> getBody() {
-            return result;
-        }
-    }
-
-    static class MultiProcessorImpl<V> implements HttpResponse.MultiProcessor<MultiMapResult<V>,V> {
-        private final MultiMapResult<V> results;
-        private final Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler;
-        private final boolean completion; // aggregate completes on last PP received or overall completion
-
-        MultiProcessorImpl(Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler, boolean completion) {
-            this.results = new MultiMapResult<V>(new ConcurrentHashMap<>());
-            this.pushHandler = pushHandler;
-            this.completion = completion;
-        }
-
-        @Override
-        public Optional<HttpResponse.BodyHandler<V>> onRequest(HttpRequest request) {
-            return pushHandler.apply(request);
-        }
-
-        @Override
-        public void onResponse(HttpResponse<V> response) {
-            results.put(response.request(), CompletableFuture.completedFuture(response));
-        }
-
-        @Override
-        public void onError(HttpRequest request, Throwable t) {
-            results.put(request, MinimalFuture.failedFuture(t));
-        }
-
-        @Override
-        public CompletableFuture<MultiMapResult<V>> completion(
-                CompletableFuture<Void> onComplete, CompletableFuture<Void> onFinalPushPromise) {
-            if (completion)
-                return onComplete.thenApply((ignored)-> results);
-            else
-                return onFinalPushPromise.thenApply((ignored) -> results);
-        }
-    }
-
-    static class MultiFile {
-
-        final Path pathRoot;
-
-        MultiFile(Path destination) {
-            if (!destination.toFile().isDirectory())
-                throw new UncheckedIOException(new IOException("destination is not a directory"));
-            pathRoot = destination;
-        }
-
-        Optional<HttpResponse.BodyHandler<Path>> handlePush(HttpRequest request) {
-            final URI uri = request.uri();
-            String path = uri.getPath();
-            while (path.startsWith("/"))
-                path = path.substring(1);
-            Path p = pathRoot.resolve(path);
-            if (Log.trace()) {
-                Log.logTrace("Creating file body processor for URI={0}, path={1}",
-                             uri, p);
-            }
-            try {
-                Files.createDirectories(p.getParent());
-            } catch (IOException ex) {
-                throw new UncheckedIOException(ex);
-            }
-
-            final HttpResponse.BodyHandler<Path> proc =
-                 HttpResponse.BodyHandler.asFile(p);
-
-            return Optional.of(proc);
-        }
-    }
-
-    /**
-     * Currently this consumes all of the data and ignores it
-     */
-    static class NullProcessor<T> implements HttpResponse.BodyProcessor<T> {
-
-        Flow.Subscription subscription;
-        final CompletableFuture<T> cf = new MinimalFuture<>();
-        final Optional<T> result;
-
-        NullProcessor(Optional<T> result) {
-            this.result = result;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            this.subscription = subscription;
-            subscription.request(Long.MAX_VALUE);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            // TODO: check whether this should consume the buffer, as in:
-            item.position(item.limit());
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            cf.completeExceptionally(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            if (result.isPresent()) {
-                cf.complete(result.get());
-            } else {
-                cf.complete(null);
-            }
-        }
-
-        @Override
-        public CompletionStage<T> getBody() {
-            return cf;
-        }
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseSubscribers.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2016, 2017, 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.incubator.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Flow;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.common.Log;
+
+class ResponseSubscribers {
+
+    static class ConsumerSubscriber implements HttpResponse.BodySubscriber<Void> {
+        private final Consumer<Optional<byte[]>> consumer;
+        private Flow.Subscription subscription;
+        private final CompletableFuture<Void> result = new MinimalFuture<>();
+
+        ConsumerSubscriber(Consumer<Optional<byte[]>> consumer) {
+            this.consumer = consumer;
+        }
+
+        @Override
+        public CompletionStage<Void> getBody() {
+            return result;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            this.subscription = subscription;
+            subscription.request(1);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            for (ByteBuffer item : items) {
+                byte[] buf = new byte[item.remaining()];
+                item.get(buf);
+                consumer.accept(Optional.of(buf));
+            }
+            subscription.request(1);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            result.completeExceptionally(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            consumer.accept(Optional.empty());
+            result.complete(null);
+        }
+
+    }
+
+    static class PathSubscriber implements HttpResponse.BodySubscriber<Path> {
+
+        private final Path file;
+        private final CompletableFuture<Path> result = new MinimalFuture<>();
+
+        private volatile Flow.Subscription subscription;
+        private volatile FileChannel out;
+        private volatile AccessControlContext acc;
+        private final OpenOption[] options;
+
+        PathSubscriber(Path file, OpenOption... options) {
+            this.file = file;
+            this.options = options;
+        }
+
+        void setAccessControlContext(AccessControlContext acc) {
+            this.acc = acc;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            if (System.getSecurityManager() != null && acc == null)
+                throw new InternalError(
+                        "Unexpected null acc when security manager has been installed");
+
+            this.subscription = subscription;
+            try {
+                PrivilegedExceptionAction<FileChannel> pa =
+                        () -> FileChannel.open(file, options);
+                out = AccessController.doPrivileged(pa, acc);
+            } catch (PrivilegedActionException pae) {
+                Throwable t = pae.getCause() != null ? pae.getCause() : pae;
+                result.completeExceptionally(t);
+                subscription.cancel();
+                return;
+            }
+            subscription.request(1);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            try {
+                out.write(items.toArray(new ByteBuffer[0]));
+            } catch (IOException ex) {
+                Utils.close(out);
+                subscription.cancel();
+                result.completeExceptionally(ex);
+            }
+            subscription.request(1);
+        }
+
+        @Override
+        public void onError(Throwable e) {
+            result.completeExceptionally(e);
+            Utils.close(out);
+        }
+
+        @Override
+        public void onComplete() {
+            Utils.close(out);
+            result.complete(file);
+        }
+
+        @Override
+        public CompletionStage<Path> getBody() {
+            return result;
+        }
+    }
+
+    static class ByteArraySubscriber<T> implements HttpResponse.BodySubscriber<T> {
+        private final Function<byte[], T> finisher;
+        private final CompletableFuture<T> result = new MinimalFuture<>();
+        private final List<ByteBuffer> received = new ArrayList<>();
+
+        private volatile Flow.Subscription subscription;
+
+        ByteArraySubscriber(Function<byte[],T> finisher) {
+            this.finisher = finisher;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            if (this.subscription != null) {
+                subscription.cancel();
+                return;
+            }
+            this.subscription = subscription;
+            // We can handle whatever you've got
+            subscription.request(Long.MAX_VALUE);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            // incoming buffers are allocated by http client internally,
+            // and won't be used anywhere except this place.
+            // So it's free simply to store them for further processing.
+            assert Utils.hasRemaining(items);
+            Utils.accumulateBuffers(received, items);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            received.clear();
+            result.completeExceptionally(throwable);
+        }
+
+        static private byte[] join(List<ByteBuffer> bytes) {
+            int size = Utils.remaining(bytes, Integer.MAX_VALUE);
+            byte[] res = new byte[size];
+            int from = 0;
+            for (ByteBuffer b : bytes) {
+                int l = b.remaining();
+                b.get(res, from, l);
+                from += l;
+            }
+            return res;
+        }
+
+        @Override
+        public void onComplete() {
+            try {
+                result.complete(finisher.apply(join(received)));
+                received.clear();
+            } catch (IllegalArgumentException e) {
+                result.completeExceptionally(e);
+            }
+        }
+
+        @Override
+        public CompletionStage<T> getBody() {
+            return result;
+        }
+    }
+
+    /**
+     * An InputStream built on top of the Flow API.
+     */
+    static class HttpResponseInputStream extends InputStream
+        implements HttpResponse.BodySubscriber<InputStream>
+    {
+        final static boolean DEBUG = Utils.DEBUG;
+        final static int MAX_BUFFERS_IN_QUEUE = 1;  // lock-step with the producer
+
+        // An immutable ByteBuffer sentinel to mark that the last byte was received.
+        private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]);
+        private static final List<ByteBuffer> LAST_LIST = List.of(LAST_BUFFER);
+        private static final System.Logger DEBUG_LOGGER =
+                Utils.getDebugLogger("HttpResponseInputStream"::toString, DEBUG);
+
+        // A queue of yet unprocessed ByteBuffers received from the flow API.
+        private final BlockingQueue<List<ByteBuffer>> buffers;
+        private volatile Flow.Subscription subscription;
+        private volatile boolean closed;
+        private volatile Throwable failed;
+        private volatile Iterator<ByteBuffer> currentListItr;
+        private volatile ByteBuffer currentBuffer;
+
+        HttpResponseInputStream() {
+            this(MAX_BUFFERS_IN_QUEUE);
+        }
+
+        HttpResponseInputStream(int maxBuffers) {
+            int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers);
+            // 1 additional slot needed for LAST_LIST added by onComplete
+            this.buffers = new ArrayBlockingQueue<>(capacity + 1);
+        }
+
+        @Override
+        public CompletionStage<InputStream> getBody() {
+            // Returns the stream immediately, before the
+            // response body is received.
+            // This makes it possible for senAsync().get().body()
+            // to complete before the response body is received.
+            return CompletableFuture.completedStage(this);
+        }
+
+        // Returns the current byte buffer to read from.
+        // If the current buffer has no remaining data, this method will take the
+        // next buffer from the buffers queue, possibly blocking until
+        // a new buffer is made available through the Flow API, or the
+        // end of the flow has been reached.
+        private ByteBuffer current() throws IOException {
+            while (currentBuffer == null || !currentBuffer.hasRemaining()) {
+                // Check whether the stream is closed or exhausted
+                if (closed || failed != null) {
+                    throw new IOException("closed", failed);
+                }
+                if (currentBuffer == LAST_BUFFER) break;
+
+                try {
+                    if (currentListItr == null || !currentListItr.hasNext()) {
+                        // Take a new list of buffers from the queue, blocking
+                        // if none is available yet...
+
+                        DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers");
+                        List<ByteBuffer> lb = buffers.take();
+                        currentListItr = lb.iterator();
+                        DEBUG_LOGGER.log(Level.DEBUG, "List of Buffers Taken");
+
+                        // Check whether an exception was encountered upstream
+                        if (closed || failed != null)
+                            throw new IOException("closed", failed);
+
+                        // Check whether we're done.
+                        if (lb == LAST_LIST) {
+                            currentListItr = null;
+                            currentBuffer = LAST_BUFFER;
+                            break;
+                        }
+
+                        // Request another upstream item ( list of buffers )
+                        Flow.Subscription s = subscription;
+                        if (s != null) {
+                            DEBUG_LOGGER.log(Level.DEBUG, "Increased demand by 1");
+                            s.request(1);
+                        }
+                    }
+                    assert currentListItr != null;
+                    assert currentListItr.hasNext();
+                    DEBUG_LOGGER.log(Level.DEBUG, "Next Buffer");
+                    currentBuffer = currentListItr.next();
+                } catch (InterruptedException ex) {
+                    // continue
+                }
+            }
+            assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining();
+            return currentBuffer;
+        }
+
+        @Override
+        public int read(byte[] bytes, int off, int len) throws IOException {
+            // get the buffer to read from, possibly blocking if
+            // none is available
+            ByteBuffer buffer;
+            if ((buffer = current()) == LAST_BUFFER) return -1;
+
+            // don't attempt to read more than what is available
+            // in the current buffer.
+            int read = Math.min(buffer.remaining(), len);
+            assert read > 0 && read <= buffer.remaining();
+
+            // buffer.get() will do the boundary check for us.
+            buffer.get(bytes, off, read);
+            return read;
+        }
+
+        @Override
+        public int read() throws IOException {
+            ByteBuffer buffer;
+            if ((buffer = current()) == LAST_BUFFER) return -1;
+            return buffer.get() & 0xFF;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription s) {
+            if (this.subscription != null) {
+                s.cancel();
+                return;
+            }
+            this.subscription = s;
+            assert buffers.remainingCapacity() > 1; // should contain at least 2
+            DEBUG_LOGGER.log(Level.DEBUG, () -> "onSubscribe: requesting "
+                                +  Math.max(1, buffers.remainingCapacity() - 1));
+            s.request(Math.max(1, buffers.remainingCapacity() - 1));
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> t) {
+            try {
+                DEBUG_LOGGER.log(Level.DEBUG, "next item received");
+                if (!buffers.offer(t)) {
+                    throw new IllegalStateException("queue is full");
+                }
+                DEBUG_LOGGER.log(Level.DEBUG, "item offered");
+            } catch (Exception ex) {
+                failed = ex;
+                try {
+                    close();
+                } catch (IOException ex1) {
+                    // OK
+                }
+            }
+        }
+
+        @Override
+        public void onError(Throwable thrwbl) {
+            subscription = null;
+            failed = thrwbl;
+        }
+
+        @Override
+        public void onComplete() {
+            subscription = null;
+            onNext(LAST_LIST);
+        }
+
+        @Override
+        public void close() throws IOException {
+            synchronized (this) {
+                if (closed) return;
+                closed = true;
+            }
+            Flow.Subscription s = subscription;
+            subscription = null;
+            if (s != null) {
+                 s.cancel();
+            }
+            super.close();
+        }
+
+    }
+
+    static class MultiSubscriberImpl<V>
+        implements HttpResponse.MultiSubscriber<MultiMapResult<V>,V>
+    {
+        private final MultiMapResult<V> results;
+        private final Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler;
+        private final Function<HttpRequest,HttpResponse.BodyHandler<V>> requestHandler;
+        private final boolean completion; // aggregate completes on last PP received or overall completion
+
+        MultiSubscriberImpl(
+                Function<HttpRequest,HttpResponse.BodyHandler<V>> requestHandler,
+                Function<HttpRequest,Optional<HttpResponse.BodyHandler<V>>> pushHandler, boolean completion) {
+            this.results = new MultiMapResult<>(new ConcurrentHashMap<>());
+            this.requestHandler = requestHandler;
+            this.pushHandler = pushHandler;
+            this.completion = completion;
+        }
+
+        @Override
+        public HttpResponse.BodyHandler<V> onRequest(HttpRequest request) {
+            CompletableFuture<HttpResponse<V>> cf = MinimalFuture.newMinimalFuture();
+            results.put(request, cf);
+            return requestHandler.apply(request);
+        }
+
+        @Override
+        public Optional<HttpResponse.BodyHandler<V>> onPushPromise(HttpRequest push) {
+            CompletableFuture<HttpResponse<V>> cf = MinimalFuture.newMinimalFuture();
+            results.put(push, cf);
+            return pushHandler.apply(push);
+        }
+
+        @Override
+        public void onResponse(HttpResponse<V> response) {
+            HttpRequest request = response.request();
+            CompletableFuture<HttpResponse<V>> cf = results.get(response.request());
+            cf.complete(response);
+        }
+
+        @Override
+        public void onError(HttpRequest request, Throwable t) {
+            CompletableFuture<HttpResponse<V>> cf = results.get(request);
+            cf.completeExceptionally(t);
+        }
+
+        @Override
+        public CompletableFuture<MultiMapResult<V>> completion(
+                CompletableFuture<Void> onComplete, CompletableFuture<Void> onFinalPushPromise) {
+            if (completion)
+                return onComplete.thenApply((ignored)-> results);
+            else
+                return onFinalPushPromise.thenApply((ignored) -> results);
+        }
+    }
+
+    static class MultiFile {
+
+        final Path pathRoot;
+
+        MultiFile(Path destination) {
+            if (!destination.toFile().isDirectory())
+                throw new UncheckedIOException(new IOException("destination is not a directory"));
+            pathRoot = destination;
+        }
+
+        Optional<HttpResponse.BodyHandler<Path>> handlePush(HttpRequest request) {
+            final URI uri = request.uri();
+            String path = uri.getPath();
+            while (path.startsWith("/"))
+                path = path.substring(1);
+            Path p = pathRoot.resolve(path);
+            if (Log.trace()) {
+                Log.logTrace("Creating file body subscriber for URI={0}, path={1}",
+                             uri, p);
+            }
+            try {
+                Files.createDirectories(p.getParent());
+            } catch (IOException ex) {
+                throw new UncheckedIOException(ex);
+            }
+
+            final HttpResponse.BodyHandler<Path> proc =
+                 HttpResponse.BodyHandler.asFile(p);
+
+            return Optional.of(proc);
+        }
+    }
+
+    /**
+     * Currently this consumes all of the data and ignores it
+     */
+    static class NullSubscriber<T> implements HttpResponse.BodySubscriber<T> {
+
+        volatile Flow.Subscription subscription;
+        final CompletableFuture<T> cf = new MinimalFuture<>();
+        final Optional<T> result;
+
+        NullSubscriber(Optional<T> result) {
+            this.result = result;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            this.subscription = subscription;
+            subscription.request(Long.MAX_VALUE);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            // NO-OP
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            cf.completeExceptionally(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            if (result.isPresent()) {
+                cf.complete(result.get());
+            } else {
+                cf.complete(null);
+            }
+        }
+
+        @Override
+        public CompletionStage<T> getBody() {
+            return cf;
+        }
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,205 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import javax.net.ssl.SSLEngineResult.Status;
-import javax.net.ssl.SSLParameters;
-import jdk.incubator.http.SSLDelegate.WrapperResult;
-
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * An SSL connection built on a Plain TCP connection.
- */
-class SSLConnection extends HttpConnection {
-
-    PlainHttpConnection delegate;
-    SSLDelegate sslDelegate;
-    final String[] alpn;
-    final String serverName;
-
-    @Override
-    public CompletableFuture<Void> connectAsync() {
-        return delegate.connectAsync()
-                .thenCompose((Void v) ->
-                                MinimalFuture.supply( () -> {
-                                    this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
-                                    return null;
-                                }));
-    }
-
-    @Override
-    public void connect() throws IOException {
-        delegate.connect();
-        this.sslDelegate = new SSLDelegate(delegate.channel(), client, alpn, serverName);
-    }
-
-    SSLConnection(InetSocketAddress addr, HttpClientImpl client, String[] ap) {
-        super(addr, client);
-        this.alpn = ap;
-        this.serverName = Utils.getServerName(addr);
-        delegate = new PlainHttpConnection(addr, client);
-    }
-
-    /**
-     * Create an SSLConnection from an existing connected AsyncSSLConnection.
-     * Used when downgrading from HTTP/2 to HTTP/1.1
-     */
-    SSLConnection(AsyncSSLConnection c) {
-        super(c.address, c.client);
-        this.delegate = c.plainConnection();
-        AsyncSSLDelegate adel = c.sslDelegate();
-        this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
-        this.alpn = adel.alpn;
-        this.serverName = adel.serverName;
-    }
-
-    @Override
-    SSLParameters sslParameters() {
-        return sslDelegate.getSSLParameters();
-    }
-
-    @Override
-    public String toString() {
-        return "SSLConnection: " + super.toString();
-    }
-
-    private static long countBytes(ByteBuffer[] buffers, int start, int length) {
-        long c = 0;
-        for (int i=0; i<length; i++) {
-            c+= buffers[start+i].remaining();
-        }
-        return c;
-    }
-
-    @Override
-    ConnectionPool.CacheKey cacheKey() {
-        return ConnectionPool.cacheKey(address, null);
-    }
-
-    @Override
-    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-        //debugPrint("Send", buffers, start, number);
-        long l = countBytes(buffers, start, number);
-        WrapperResult r = sslDelegate.sendData(buffers, start, number);
-        if (r.result.getStatus() == Status.CLOSED) {
-            if (l > 0) {
-                throw new IOException("SSLHttpConnection closed");
-            }
-        }
-        return l;
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        //debugPrint("Send", buffer);
-        long l = buffer.remaining();
-        WrapperResult r = sslDelegate.sendData(buffer);
-        if (r.result.getStatus() == Status.CLOSED) {
-            if (l > 0) {
-                throw new IOException("SSLHttpConnection closed");
-            }
-        }
-        return l;
-    }
-
-    @Override
-    void writeAsync(ByteBufferReference[] buffers) throws IOException {
-        write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
-    }
-
-    @Override
-    void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
-    }
-
-    @Override
-    void flushAsync() throws IOException {
-        // nothing to do
-    }
-
-    @Override
-    public void close() {
-        Utils.close(delegate.channel());
-    }
-
-    @Override
-    void shutdownInput() throws IOException {
-        delegate.channel().shutdownInput();
-    }
-
-    @Override
-    void shutdownOutput() throws IOException {
-        delegate.channel().shutdownOutput();
-    }
-
-    @Override
-    protected ByteBuffer readImpl() throws IOException {
-        WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
-        // TODO: check for closure
-        int n = r.result.bytesProduced();
-        if (n > 0) {
-            return r.buf;
-        } else if (n == 0) {
-            return Utils.EMPTY_BYTEBUFFER;
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    boolean connected() {
-        return delegate.connected();
-    }
-
-    @Override
-    SocketChannel channel() {
-        return delegate.channel();
-    }
-
-    @Override
-    CompletableFuture<Void> whenReceivingResponse() {
-        return delegate.whenReceivingResponse();
-    }
-
-    @Override
-    boolean isSecure() {
-        return true;
-    }
-
-    @Override
-    boolean isProxied() {
-        return false;
-    }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java	Sun Nov 05 17:32:13 2017 +0000
@@ -41,7 +41,10 @@
 
 /**
  * Implements the mechanics of SSL by managing an SSLEngine object.
- * One of these is associated with each SSLConnection.
+ * <p>
+ * This class is only used to implement the {@link
+ * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
+ * to RawChannelImpl when creating a WebSocket.
  */
 class SSLDelegate {
 
@@ -71,8 +74,7 @@
         SSLContext context = client.sslContext();
         engine = context.createSSLEngine();
         engine.setUseClientMode(true);
-        SSLParameters sslp = client.sslParameters()
-                                   .orElseGet(context::getSupportedSSLParameters);
+        SSLParameters sslp = client.sslParameters();
         sslParameters = Utils.copySSLParameters(sslp);
         if (sn != null) {
             SNIHostName sni = new SNIHostName(sn);
@@ -94,7 +96,7 @@
         return sslParameters;
     }
 
-    private static long countBytes(ByteBuffer[] buffers, int start, int number) {
+    static long countBytes(ByteBuffer[] buffers, int start, int number) {
         long c = 0;
         for (int i=0; i<number; i++) {
             c+= buffers[start+i].remaining();
@@ -191,7 +193,8 @@
 
         SocketChannel chan;
         SSLEngine engine;
-        Object wrapLock, unwrapLock;
+        final Object wrapLock;
+        final Object unwrapLock;
         ByteBuffer unwrap_src, wrap_dst;
         boolean closed = false;
         int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
@@ -407,7 +410,7 @@
      */
     @SuppressWarnings("fallthrough")
     void doHandshake (HandshakeStatus hs_status) throws IOException {
-        boolean wasBlocking = false;
+        boolean wasBlocking;
         try {
             wasBlocking = chan.isBlocking();
             handshaking.lock();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import javax.net.ssl.SSLEngineResult.Status;
-import javax.net.ssl.SSLParameters;
-import jdk.incubator.http.SSLDelegate.WrapperResult;
-
-import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * An SSL tunnel built on a Plain (CONNECT) TCP tunnel.
- */
-class SSLTunnelConnection extends HttpConnection {
-
-    final PlainTunnelingConnection delegate;
-    protected SSLDelegate sslDelegate;
-    private volatile boolean connected;
-    final String serverName;
-
-    @Override
-    public void connect() throws IOException, InterruptedException {
-        delegate.connect();
-        this.sslDelegate = new SSLDelegate(delegate.channel(), client, null, serverName);
-        connected = true;
-    }
-
-    @Override
-    boolean connected() {
-        return connected && delegate.connected();
-    }
-
-    @Override
-    public CompletableFuture<Void> connectAsync() {
-        return delegate.connectAsync()
-            .thenAccept((Void v) -> {
-                try {
-                    // can this block?
-                    this.sslDelegate = new SSLDelegate(delegate.channel(),
-                                                       client,
-                                                       null, serverName);
-                    connected = true;
-                } catch (IOException e) {
-                    throw new UncheckedIOException(e);
-                }
-            });
-    }
-
-    SSLTunnelConnection(InetSocketAddress addr,
-                        HttpClientImpl client,
-                        InetSocketAddress proxy)
-    {
-        super(addr, client);
-        this.serverName = Utils.getServerName(addr);
-        delegate = new PlainTunnelingConnection(addr, proxy, client);
-    }
-
-    /**
-     * Create an SSLTunnelConnection from an existing connected AsyncSSLTunnelConnection.
-     * Used when downgrading from HTTP/2 to HTTP/1.1
-     */
-    SSLTunnelConnection(AsyncSSLTunnelConnection c) {
-        super(c.address, c.client);
-        this.delegate = c.plainConnection();
-        AsyncSSLDelegate adel = c.sslDelegate();
-        this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
-        this.serverName = adel.serverName;
-        connected = c.connected();
-    }
-
-    @Override
-    SSLParameters sslParameters() {
-        return sslDelegate.getSSLParameters();
-    }
-
-    @Override
-    public String toString() {
-        return "SSLTunnelConnection: " + super.toString();
-    }
-
-    private static long countBytes(ByteBuffer[] buffers, int start, int number) {
-        long c = 0;
-        for (int i=0; i<number; i++) {
-            c+= buffers[start+i].remaining();
-        }
-        return c;
-    }
-
-    @Override
-    ConnectionPool.CacheKey cacheKey() {
-        return ConnectionPool.cacheKey(address, delegate.proxyAddr);
-    }
-
-    @Override
-    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-        //debugPrint("Send", buffers, start, number);
-        long l = countBytes(buffers, start, number);
-        WrapperResult r = sslDelegate.sendData(buffers, start, number);
-        if (r.result.getStatus() == Status.CLOSED) {
-            if (l > 0) {
-                throw new IOException("SSLHttpConnection closed");
-            }
-        }
-        return l;
-    }
-
-    @Override
-    long write(ByteBuffer buffer) throws IOException {
-        //debugPrint("Send", buffer);
-        long l = buffer.remaining();
-        WrapperResult r = sslDelegate.sendData(buffer);
-        if (r.result.getStatus() == Status.CLOSED) {
-            if (l > 0) {
-                throw new IOException("SSLHttpConnection closed");
-            }
-        }
-        return l;
-    }
-
-    @Override
-    void writeAsync(ByteBufferReference[] buffers) throws IOException {
-        write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
-    }
-
-    @Override
-    void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-        write(ByteBufferReference.toBuffers(buffers), 0, buffers.length);
-    }
-
-    @Override
-    void flushAsync() throws IOException {
-        // nothing to do
-    }
-
-    @Override
-    public void close() {
-        Utils.close(delegate.channel());
-    }
-
-    @Override
-    void shutdownInput() throws IOException {
-        delegate.channel().shutdownInput();
-    }
-
-    @Override
-    void shutdownOutput() throws IOException {
-        delegate.channel().shutdownOutput();
-    }
-
-    @Override
-    protected ByteBuffer readImpl() throws IOException {
-        ByteBuffer buf = Utils.getBuffer();
-
-        WrapperResult r = sslDelegate.recvData(buf);
-        return r.buf;
-    }
-
-    @Override
-    SocketChannel channel() {
-        return delegate.channel();
-    }
-
-    @Override
-    CompletableFuture<Void> whenReceivingResponse() {
-        return delegate.whenReceivingResponse();
-    }
-
-    @Override
-    boolean isSecure() {
-        return true;
-    }
-
-    @Override
-    boolean isProxied() {
-        return true;
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SocketTube.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,956 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.SequentialScheduler.RestartableTask;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * A SocketTube is a terminal tube plugged directly into the socket.
+ * The read subscriber should call {@code subscribe} on the SocketTube before
+ * the SocketTube can be subscribed to the write publisher.
+ */
+final class SocketTube implements FlowTube {
+
+    static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+    static final AtomicLong IDS = new AtomicLong();
+
+    private final HttpClientImpl client;
+    private final SocketChannel channel;
+    private final Supplier<ByteBuffer> buffersSource;
+    private final Object lock = new Object();
+    private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+    private final InternalReadPublisher readPublisher;
+    private final InternalWriteSubscriber writeSubscriber;
+    private final long id = IDS.incrementAndGet();
+
+    public SocketTube(HttpClientImpl client, SocketChannel channel,
+                      Supplier<ByteBuffer> buffersSource) {
+        this.client = client;
+        this.channel = channel;
+        this.buffersSource = buffersSource;
+        this.readPublisher = new InternalReadPublisher();
+        this.writeSubscriber = new InternalWriteSubscriber();
+    }
+
+    private static Flow.Subscription nopSubscription() {
+        return new Flow.Subscription() {
+            @Override public void request(long n) { }
+            @Override public void cancel() { }
+        };
+    }
+
+    /**
+     * Returns {@code true} if this flow is finished.
+     * This happens when this flow internal read subscription is completed,
+     * either normally (EOF reading) or exceptionally  (EOF writing, or
+     * underlying socket closed, or some exception occurred while reading or
+     * writing to the socket).
+     *
+     * @return {@code true} if this flow is finished.
+     */
+    public boolean isFinished() {
+        InternalReadPublisher.InternalReadSubscription subscription =
+                readPublisher.subscriptionImpl;
+        return subscription != null && subscription.completed
+                || subscription == null && errorRef.get() != null;
+    }
+
+    // ===================================================================== //
+    //                       Flow.Publisher                                  //
+    // ======================================================================//
+
+    /**
+     * {@inheritDoc }
+     * @apiNote This method should be called first. In particular, the caller
+     *          must ensure that this method must be called by the read
+     *          subscriber before the write publisher can call {@code onSubscribe}.
+     *          Failure to adhere to this contract may result in assertion errors.
+     */
+    @Override
+    public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+        Objects.requireNonNull(s);
+        assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
+        readPublisher.subscribe(s);
+    }
+
+
+    // ===================================================================== //
+    //                       Flow.Subscriber                                 //
+    // ======================================================================//
+
+    /**
+     * {@inheritDoc }
+     * @apiNote The caller must ensure that {@code subscribe} is called by
+     *          the read subscriber before {@code onSubscribe} is called by
+     *          the write publisher.
+     *          Failure to adhere to this contract may result in assertion errors.
+     */
+    @Override
+    public void onSubscribe(Flow.Subscription subscription) {
+        writeSubscriber.onSubscribe(subscription);
+    }
+
+    @Override
+    public void onNext(List<ByteBuffer> item) {
+        writeSubscriber.onNext(item);
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+        writeSubscriber.onError(throwable);
+    }
+
+    @Override
+    public void onComplete() {
+        writeSubscriber.onComplete();
+    }
+
+    // ===================================================================== //
+    //                           Events                                      //
+    // ======================================================================//
+
+    /**
+     * A restartable task used to process tasks in sequence.
+     */
+    private static class SocketFlowTask implements RestartableTask {
+        final Runnable task;
+        private final Object monitor = new Object();
+        SocketFlowTask(Runnable task) {
+            this.task = task;
+        }
+        @Override
+        public final void run(DeferredCompleter taskCompleter) {
+            try {
+                // non contentious synchronized for visibility.
+                synchronized(monitor) {
+                    task.run();
+                }
+            } finally {
+                taskCompleter.complete();
+            }
+        }
+    }
+
+    // This is best effort - there's no guarantee that the printed set
+    // of values is consistent. It should only be considered as
+    // weakly accurate - in particular in what concerns the events states,
+    // especially when displaying a read event state from a write event
+    // callback and conversely.
+    void debugState(String when) {
+        if (debug.isLoggable(Level.DEBUG)) {
+            StringBuilder state = new StringBuilder();
+
+            InternalReadPublisher.InternalReadSubscription sub =
+                    readPublisher.subscriptionImpl;
+            InternalReadPublisher.ReadEvent readEvent =
+                    sub == null ? null : sub.readEvent;
+            Demand rdemand = sub == null ? null : sub.demand;
+            InternalWriteSubscriber.WriteEvent writeEvent =
+                    writeSubscriber.writeEvent;
+            AtomicLong wdemand = writeSubscriber.writeDemand;
+            int rops = readEvent == null ? 0 : readEvent.interestOps();
+            long rd = rdemand == null ? 0 : rdemand.get();
+            int wops = writeEvent == null ? 0 : writeEvent.interestOps();
+            long wd = wdemand == null ? 0 : wdemand.get();
+
+            state.append(when).append(" Reading: [ops=")
+                    .append(rops).append(", demand=").append(rd)
+                    .append(", stopped=")
+                    .append((sub == null ? false : sub.readScheduler.isStopped()))
+                    .append("], Writing: [ops=").append(wops)
+                    .append(", demand=").append(wd)
+                    .append("]");
+            debug.log(Level.DEBUG, state.toString());
+        }
+    }
+
+    /**
+     * A repeatable event that can be paused or resumed by changing
+     * its interestOps.
+     * When the event is fired, it is first paused before being signaled.
+     * It is the responsibility of the code triggered by {@code signalEvent}
+     * to resume the event if required.
+     */
+    private static abstract class SocketFlowEvent extends AsyncEvent {
+        final SocketChannel channel;
+        final int defaultInterest;
+        volatile int interestOps;
+        volatile boolean registered;
+        SocketFlowEvent(int defaultInterest, SocketChannel channel) {
+            super(AsyncEvent.REPEATING);
+            this.defaultInterest = defaultInterest;
+            this.channel = channel;
+        }
+        final boolean registered() {return registered;}
+        final void resume() {
+            interestOps = defaultInterest;
+            registered = true;
+        }
+        final void pause() {interestOps = 0;}
+        @Override
+        public final SelectableChannel channel() {return channel;}
+        @Override
+        public final int interestOps() {return interestOps;}
+
+        @Override
+        public final void handle() {
+            pause();       // pause, then signal
+            signalEvent(); // won't be fired again until resumed.
+        }
+        @Override
+        public final void abort(IOException error) {
+            debug().log(Level.DEBUG, () -> "abort: " + error);
+            pause();              // pause, then signal
+            signalError(error);   // should not be resumed after abort (not checked)
+        }
+
+        protected abstract void signalEvent();
+        protected abstract void signalError(Throwable error);
+        abstract System.Logger debug();
+    }
+
+    // ===================================================================== //
+    //                              Writing                                  //
+    // ======================================================================//
+
+    // This class makes the assumption that the publisher will call
+    // onNext sequentially, and that onNext won't be called if the demand
+    // has not been incremented by request(1).
+    // It has a 'queue of 1' meaning that it will call request(1) in
+    // onSubscribe, and then only after its 'current' buffer list has been
+    // fully written and current set to null;
+    private final class InternalWriteSubscriber
+            implements Flow.Subscriber<List<ByteBuffer>> {
+
+        volatile Flow.Subscription subscription;
+        volatile List<ByteBuffer> current;
+        volatile boolean completed;
+        final WriteEvent writeEvent = new WriteEvent(channel, this);
+        final AtomicLong writeDemand = new AtomicLong();
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            Flow.Subscription previous = this.subscription;
+            this.subscription = subscription;
+            debug.log(Level.DEBUG, "subscribed for writing");
+            if (current == null) {
+                if (previous == subscription || previous == null) {
+                    if (writeDemand.compareAndSet(0, 1)) {
+                        subscription.request(1);
+                    }
+                } else {
+                    writeDemand.set(1);
+                    subscription.request(1);
+                }
+            }
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> bufs) {
+            assert current == null; // this is a queue of 1.
+            assert subscription != null;
+            current = bufs;
+            tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
+            // For instance in HTTP/2, a received SETTINGS frame might trigger
+            // the sending of a SETTINGS frame in turn which might cause
+            // onNext to be called from within the same selector thread that the
+            // original SETTINGS frames arrived on. If rs is the read-subscriber
+            // and ws is the write-subscriber then the following can occur:
+            // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
+            // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
+            debugState("leaving w.onNext");
+        }
+
+        // we don't use a SequentialScheduler here: we rely on
+        // onNext() being called sequentially, and not being called
+        // if we haven't call request(1)
+        // onNext is usually called from within a user/executor thread.
+        // we will perform the initial writing in that thread.
+        // if for some reason, not all data can be written, the writeEvent
+        // will be resumed, and the rest of the data will be written from
+        // the selector manager thread when the writeEvent is fired.
+        // If we are in the selector manager thread, then we will use the executor
+        // to call request(1), ensuring that onNext() won't be called from
+        // within the selector thread.
+        // If we are not in the selector manager thread, then we don't care.
+        void tryFlushCurrent(boolean inSelectorThread) {
+            List<ByteBuffer> bufs = current;
+            if (bufs == null) return;
+            try {
+                assert inSelectorThread == client.isSelectorThread() :
+                       "should " + (inSelectorThread ? "" : "not ")
+                        + " be in the selector thread";
+                long remaining = Utils.remaining(bufs);
+                debug.log(Level.DEBUG, "trying to write: %d", remaining);
+                long written = writeAvailable(bufs);
+                debug.log(Level.DEBUG, "wrote: %d", remaining);
+                if (written == -1) {
+                    signalError(new EOFException("EOF reached while writing"));
+                    return;
+                }
+                assert written <= remaining;
+                if (remaining - written == 0) {
+                    current = null;
+                    writeDemand.decrementAndGet();
+                    Runnable requestMore = this::requestMore;
+                    if (inSelectorThread) {
+                        assert client.isSelectorThread();
+                        client.theExecutor().execute(requestMore);
+                    } else {
+                        assert !client.isSelectorThread();
+                        requestMore.run();
+                    }
+                } else {
+                    resumeWriteEvent(inSelectorThread);
+                }
+            } catch (Throwable t) {
+                signalError(t);
+                subscription.cancel();
+            }
+        }
+
+        void requestMore() {
+            try {
+                if (completed) return;
+                long d =  writeDemand.get();
+                if (writeDemand.compareAndSet(0,1)) {
+                    debug.log(Level.DEBUG, "write: requesting more...");
+                    subscription.request(1);
+                } else {
+                    debug.log(Level.DEBUG, "write: no need to request more: %d", d);
+                }
+            } catch (Throwable t) {
+                debug.log(Level.DEBUG, () ->
+                        "write: error while requesting more: " + t);
+                signalError(t);
+                subscription.cancel();
+            } finally {
+                debugState("leaving requestMore: ");
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            signalError(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            completed = true;
+            // no need to pause the write event here: the write event will
+            // be paused if there is nothing more to write.
+            List<ByteBuffer> bufs = current;
+            long remaining = bufs == null ? 0 : Utils.remaining(bufs);
+            debug.log(Level.DEBUG,  "write completed, %d yet to send", remaining);
+            debugState("InternalWriteSubscriber::onComplete");
+        }
+
+        void resumeWriteEvent(boolean inSelectorThread) {
+            debug.log(Level.DEBUG, "scheduling write event");
+            resumeEvent(writeEvent, this::signalError);
+        }
+
+        void pauseWriteEvent() {
+            debug.log(Level.DEBUG, "pausing write event");
+            pauseEvent(writeEvent, this::signalError);
+        }
+
+        void signalWritable() {
+            debug.log(Level.DEBUG, "channel is writable");
+            tryFlushCurrent(true);
+        }
+
+        void signalError(Throwable error) {
+            debug.log(Level.DEBUG, () -> "write error: " + error);
+            completed = true;
+            readPublisher.signalError(error);
+        }
+
+        // A repeatable WriteEvent which is paused after firing and can
+        // be resumed if required - see SocketFlowEvent;
+        final class WriteEvent extends SocketFlowEvent {
+            final InternalWriteSubscriber sub;
+            WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
+                super(SelectionKey.OP_WRITE, channel);
+                this.sub = sub;
+            }
+            @Override
+            protected final void signalEvent() {
+                try {
+                    client.eventUpdated(this);
+                    sub.signalWritable();
+                } catch(Throwable t) {
+                    sub.signalError(t);
+                }
+            }
+
+            @Override
+            protected void signalError(Throwable error) {
+                sub.signalError(error);
+            }
+
+            @Override
+            System.Logger debug() {
+                return debug;
+            }
+
+        }
+
+    }
+
+    // ===================================================================== //
+    //                              Reading                                  //
+    // ===================================================================== //
+
+    // The InternalReadPublisher uses a SequentialScheduler to ensure that
+    // onNext/onError/onComplete are called sequentially on the caller's
+    // subscriber.
+    // However, it relies on the fact that the only time where
+    // runOrSchedule() is called from a user/executor thread is in signalError,
+    // right after the errorRef has been set.
+    // Because the sequential scheduler's task always checks for errors first,
+    // and always terminate the scheduler on error, then it is safe to assume
+    // that if it reaches the point where it reads from the channel, then
+    // it is running in the SelectorManager thread. This is because all
+    // other invocation of runOrSchedule() are triggered from within a
+    // ReadEvent.
+    //
+    // When pausing/resuming the event, some shortcuts can then be taken
+    // when we know we're running in the selector manager thread
+    // (in that case there's no need to call client.eventUpdated(readEvent);
+    //
+    private final class InternalReadPublisher
+            implements Flow.Publisher<List<ByteBuffer>> {
+        private final InternalReadSubscription subscriptionImpl
+                = new InternalReadSubscription();
+        AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
+        private volatile ReadSubscription subscription;
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+            Objects.requireNonNull(s);
+
+            TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
+            ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
+            ReadSubscription previous = pendingSubscription.getAndSet(target);
+
+            if (previous != null && previous != target) {
+                debug.log(Level.DEBUG,
+                        () -> "read publisher: dropping pending subscriber: "
+                        + previous.subscriber);
+                previous.errorRef.compareAndSet(null, errorRef.get());
+                previous.signalOnSubscribe();
+                if (subscriptionImpl.completed) {
+                    previous.signalCompletion();
+                } else {
+                    previous.subscriber.dropSubscription();
+                }
+            }
+
+            debug.log(Level.DEBUG, "read publisher got subscriber");
+            subscriptionImpl.signalSubscribe();
+            debugState("leaving read.subscribe: ");
+        }
+
+        void signalError(Throwable error) {
+            if (!errorRef.compareAndSet(null, error)) {
+                return;
+            }
+            subscriptionImpl.handleError();
+        }
+
+        final class ReadSubscription implements Flow.Subscription {
+            final InternalReadSubscription impl;
+            final TubeSubscriber  subscriber;
+            final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+            volatile boolean subscribed;
+            volatile boolean cancelled;
+            volatile boolean completed;
+
+            public ReadSubscription(InternalReadSubscription impl,
+                                    TubeSubscriber subscriber) {
+                this.impl = impl;
+                this.subscriber = subscriber;
+            }
+
+            @Override
+            public void cancel() {
+                cancelled = true;
+            }
+
+            @Override
+            public void request(long n) {
+                if (!cancelled) {
+                    impl.request(n);
+                } else {
+                    debug.log(Level.DEBUG,
+                              "subscription cancelled, ignoring request %d", n);
+                }
+            }
+
+            void signalCompletion() {
+                assert subscribed || cancelled;
+                if (completed || cancelled) return;
+                synchronized (this) {
+                    if (completed) return;
+                    completed = true;
+                }
+                Throwable error = errorRef.get();
+                if (error != null) {
+                    debug.log(Level.DEBUG, () ->
+                        "forwarding error to subscriber: "
+                        + error);
+                    subscriber.onError(error);
+                } else {
+                    debug.log(Level.DEBUG, "completing subscriber");
+                    subscriber.onComplete();
+                }
+            }
+
+            void signalOnSubscribe() {
+                if (subscribed || cancelled) return;
+                synchronized (this) {
+                    if (subscribed || cancelled) return;
+                    subscribed = true;
+                }
+                subscriber.onConnection(this);
+                debug.log(Level.DEBUG, "onConnection called");
+                if (errorRef.get() != null) {
+                    signalCompletion();
+                }
+            }
+        }
+
+        final class InternalReadSubscription implements Flow.Subscription {
+
+            private final Demand demand = new Demand();
+            final SequentialScheduler readScheduler;
+            private volatile boolean completed;
+            private final ReadEvent readEvent;
+            private final AsyncEvent subscribeEvent;
+
+            InternalReadSubscription() {
+                readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
+                subscribeEvent = new AsyncTriggerEvent(this::signalError,
+                                                       this::handleSubscribeEvent);
+                readEvent = new ReadEvent(channel, this);
+            }
+
+            /*
+             * This method must be invoked before any other method of this class.
+             */
+            final void signalSubscribe() {
+                if (readScheduler.isStopped() || completed) {
+                    // if already completed or stopped we can handle any
+                    // pending connection directly from here.
+                    debug.log(Level.DEBUG,
+                              "handling pending subscription while completed");
+                    handlePending();
+                } else {
+                    try {
+                        debug.log(Level.DEBUG,
+                                  "registering subscribe event");
+                        client.registerEvent(subscribeEvent);
+                    } catch (Throwable t) {
+                        signalError(t);
+                        handlePending();
+                    }
+                }
+            }
+
+            final void handleSubscribeEvent() {
+                assert client.isSelectorThread();
+                debug.log(Level.DEBUG, "subscribe event raised");
+                readScheduler.runOrSchedule();
+                if (readScheduler.isStopped() || completed) {
+                    // if already completed or stopped we can handle any
+                    // pending connection directly from here.
+                    debug.log(Level.DEBUG,
+                              "handling pending subscription when completed");
+                    handlePending();
+                }
+            }
+
+
+            /*
+             * Although this method is thread-safe, the Reactive-Streams spec seems
+             * to not require it to be as such. It's a responsibility of the
+             * subscriber to signal demand in a thread-safe manner.
+             *
+             * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
+             * (rules 2.7 and 3.4)
+             */
+            @Override
+            public final void request(long n) {
+                if (n > 0L) {
+                    boolean wasFulfilled = demand.increase(n);
+                    if (wasFulfilled) {
+                        debug.log(Level.DEBUG, "got some demand for reading");
+                        resumeReadEvent();
+                        // if demand has been changed from fulfilled
+                        // to unfulfilled register read event;
+                    }
+                } else {
+                    signalError(new IllegalArgumentException("non-positive request"));
+                }
+                debugState("leaving request("+n+"): ");
+            }
+
+            @Override
+            public final void cancel() {
+                pauseReadEvent();
+                readScheduler.stop();
+            }
+
+            private void resumeReadEvent() {
+                debug.log(Level.DEBUG, "resuming read event");
+                resumeEvent(readEvent, this::signalError);
+            }
+
+            private void pauseReadEvent() {
+                debug.log(Level.DEBUG, "pausing read event");
+                pauseEvent(readEvent, this::signalError);
+            }
+
+
+            final void handleError() {
+                assert errorRef.get() != null;
+                readScheduler.runOrSchedule();
+            }
+
+            final void signalError(Throwable error) {
+                if (!errorRef.compareAndSet(null, error)) {
+                    return;
+                }
+                debug.log(Level.DEBUG, () -> "got read error: " + error);
+                readScheduler.runOrSchedule();
+            }
+
+            final void signalReadable() {
+                readScheduler.runOrSchedule();
+            }
+
+            /** The body of the task that runs in SequentialScheduler. */
+            final void read() {
+                // It is important to only call pauseReadEvent() when stopping
+                // the scheduler. The event is automatically paused before
+                // firing, and trying to pause it again could cause a race
+                // condition between this loop, which calls tryDecrementDemand(),
+                // and the thread that calls request(n), which will try to resume
+                // reading.
+                try {
+                    while(!readScheduler.isStopped()) {
+                        if (completed) return;
+
+                        // make sure we have a subscriber
+                        if (handlePending()) {
+                            debug.log(Level.DEBUG, "pending subscriber subscribed");
+                            return;
+                        }
+
+                        // If an error was signaled, we might not be in the
+                        // the selector thread, and that is OK, because we
+                        // will just call onError and return.
+                        ReadSubscription current = subscription;
+                        TubeSubscriber subscriber = current.subscriber;
+                        Throwable error = errorRef.get();
+                        if (error != null) {
+                            completed = true;
+                            // safe to pause here because we're finished anyway.
+                            pauseReadEvent();
+                            debug.log(Level.DEBUG, () -> "Sending error " + error
+                                  + " to subscriber " + subscriber);
+                            current.errorRef.compareAndSet(null, error);
+                            current.signalCompletion();
+                            readScheduler.stop();
+                            debugState("leaving read() loop with error: ");
+                            return;
+                        }
+
+                        // If we reach here then we must be in the selector thread.
+                        assert client.isSelectorThread();
+                        if (demand.tryDecrement()) {
+                            // we have demand.
+                            try {
+                                List<ByteBuffer> bytes = readAvailable();
+                                if (bytes == EOF) {
+                                    if (!completed) {
+                                        debug.log(Level.DEBUG, "got read EOF");
+                                        completed = true;
+                                        // safe to pause here because we're finished
+                                        // anyway.
+                                        pauseReadEvent();
+                                        current.signalCompletion();
+                                        readScheduler.stop();
+                                    }
+                                    debugState("leaving read() loop after EOF: ");
+                                    return;
+                                } else if (Utils.remaining(bytes) > 0) {
+                                    // the subscriber is responsible for offloading
+                                    // to another thread if needed.
+                                    debug.log(Level.DEBUG, () -> "read bytes: "
+                                            + Utils.remaining(bytes));
+                                    assert !current.completed;
+                                    subscriber.onNext(bytes);
+                                    // we could continue looping until the demand
+                                    // reaches 0. However, that would risk starving
+                                    // other connections (bound to other socket
+                                    // channels) - as other selected keys activated
+                                    // by the selector manager thread might be
+                                    // waiting for this event to terminate.
+                                    // So resume the read event and return now...
+                                    resumeReadEvent();
+                                    debugState("leaving read() loop after onNext: ");
+                                    return;
+                                } else {
+                                    // nothing available!
+                                    debug.log(Level.DEBUG, "no more bytes available");
+                                    // re-increment the demand and resume the read
+                                    // event. This ensures that this loop is
+                                    // executed again when the socket becomes
+                                    // readable again.
+                                    demand.increase(1);
+                                    resumeReadEvent();
+                                    debugState("leaving read() loop with no bytes");
+                                    return;
+                                }
+                            } catch (Throwable x) {
+                                signalError(x);
+                                continue;
+                            }
+                        } else {
+                            debug.log(Level.DEBUG, "no more demand for reading");
+                            // the event is paused just after firing, so it should
+                            // still be paused here, unless the demand was just
+                            // incremented from 0 to n, in which case, the
+                            // event will be resumed, causing this loop to be
+                            // invoked again when the socket becomes readable:
+                            // This is what we want.
+                            // Trying to pause the event here would actually
+                            // introduce a race condition between this loop and
+                            // request(n).
+                            debugState("leaving read() loop with no demand");
+                            break;
+                        }
+                    }
+                } catch (Throwable t) {
+                    debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
+                    signalError(t);
+                } finally {
+                    handlePending();
+                }
+            }
+
+            boolean handlePending() {
+                ReadSubscription pending = pendingSubscription.getAndSet(null);
+                if (pending == null) return false;
+                debug.log(Level.DEBUG, "handling pending subscription for %s",
+                          pending.subscriber);
+                ReadSubscription current = subscription;
+                if (current != null && current != pending && !completed) {
+                    current.subscriber.dropSubscription();
+                }
+                debug.log(Level.DEBUG, "read demand reset to 0");
+                subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
+                pending.errorRef.compareAndSet(null, errorRef.get());
+                if (!readScheduler.isStopped()) {
+                    subscription = pending;
+                } else {
+                    debug.log(Level.DEBUG, "socket tube is already stopped");
+                }
+                debug.log(Level.DEBUG, "calling onSubscribe");
+                pending.signalOnSubscribe();
+                if (completed) {
+                    pending.errorRef.compareAndSet(null, errorRef.get());
+                    pending.signalCompletion();
+                }
+                return true;
+            }
+        }
+
+
+        // A repeatable ReadEvent which is paused after firing and can
+        // be resumed if required - see SocketFlowEvent;
+        final class ReadEvent extends SocketFlowEvent {
+            final InternalReadSubscription sub;
+            ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
+                super(SelectionKey.OP_READ, channel);
+                this.sub = sub;
+            }
+            @Override
+            protected final void signalEvent() {
+                try {
+                    client.eventUpdated(this);
+                    sub.signalReadable();
+                } catch(Throwable t) {
+                    sub.signalError(t);
+                }
+            }
+
+            @Override
+            protected final void signalError(Throwable error) {
+                sub.signalError(error);
+            }
+
+            @Override
+            System.Logger debug() {
+                return debug;
+            }
+        }
+
+    }
+
+    // ===================================================================== //
+    //                   Socket Channel Read/Write                           //
+    // ===================================================================== //
+    static final int MAX_BUFFERS = 3;
+    static final List<ByteBuffer> EOF = List.of();
+
+    private List<ByteBuffer> readAvailable() throws IOException {
+        ByteBuffer buf = buffersSource.get();
+        assert buf.hasRemaining();
+
+        int read;
+        int pos = buf.position();
+        List<ByteBuffer> list = null;
+        while (buf.hasRemaining()) {
+            while ((read = channel.read(buf)) > 0) {
+               if (!buf.hasRemaining()) break;
+            }
+
+            // nothing read;
+            if (buf.position() == pos) {
+                // An empty list signal the end of data, and should only be
+                // returned if read == -1.
+                // If we already read some data, then we must return what we have
+                // read, and -1 will be returned next time the caller attempts to
+                // read something.
+                if (list == null && read == -1) {  // eof
+                    list = EOF;
+                    break;
+                }
+            }
+            buf.limit(buf.position());
+            buf.position(pos);
+            if (list == null) {
+                list = List.of(buf);
+            } else {
+                if (!(list instanceof ArrayList)) {
+                    list = new ArrayList<>(list);
+                }
+                list.add(buf);
+            }
+            if (read <= 0 || list.size() == MAX_BUFFERS) break;
+            buf = buffersSource.get();
+            pos = buf.position();
+            assert buf.hasRemaining();
+        }
+        return list;
+    }
+
+    private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
+        ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
+        final long remaining = Utils.remaining(srcs);
+        long written = 0;
+        while (remaining > written) {
+            long w = channel.write(srcs);
+            if (w == -1 && written == 0) return -1;
+            if (w == 0) break;
+            written += w;
+        }
+        return written;
+    }
+
+    private void resumeEvent(SocketFlowEvent event,
+                             Consumer<Throwable> errorSignaler) {
+        boolean registrationRequired;
+        synchronized(lock) {
+            registrationRequired = !event.registered();
+            event.resume();
+        }
+        try {
+            if (registrationRequired) {
+                client.registerEvent(event);
+             } else {
+                client.eventUpdated(event);
+            }
+        } catch(Throwable t) {
+            errorSignaler.accept(t);
+        }
+   }
+
+    private void pauseEvent(SocketFlowEvent event,
+                            Consumer<Throwable> errorSignaler) {
+        synchronized(lock) {
+            event.pause();
+        }
+        try {
+            client.eventUpdated(event);
+        } catch(Throwable t) {
+            errorSignaler.accept(t);
+        }
+    }
+
+    @Override
+    public void connectFlows(TubePublisher writePublisher,
+                             TubeSubscriber readSubscriber) {
+        debug.log(Level.DEBUG, "connecting flows");
+        this.subscribe(readSubscriber);
+        writePublisher.subscribe(this);
+    }
+
+
+    @Override
+    public String toString() {
+        return dbgString();
+    }
+
+    final String dbgString() {
+        return "SocketTube("+id+")";
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,24 +26,28 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Flow;
 import java.util.concurrent.Flow.Subscription;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Consumer;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 import jdk.incubator.http.internal.common.*;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.SynchronizedRestartableTask;
 import jdk.incubator.http.internal.frame.*;
 import jdk.incubator.http.internal.hpack.DecodingCallback;
+import static java.util.stream.Collectors.toList;
 
 /**
  * Http/2 Stream handling.
@@ -54,10 +58,6 @@
  *
  * sendRequest() -- sendHeadersOnly() + sendBody()
  *
- * sendBody() -- in calling thread: obeys all flow control (so may block)
- *               obtains data from request body processor and places on connection
- *               outbound Q.
- *
  * sendBodyAsync() -- calls sendBody() in an executor thread.
  *
  * sendHeadersAsync() -- calls sendHeadersOnly() which does not block
@@ -77,10 +77,6 @@
  *
  * getResponse() -- calls getResponseAsync() and waits for CF to complete
  *
- * responseBody() -- in calling thread: blocks for incoming DATA frames on
- *               stream inputQ. Obeys remote and local flow control so may block.
- *               Calls user response body processor with data buffers.
- *
  * responseBodyAsync() -- calls responseBody() in an executor thread.
  *
  * incoming() -- entry point called from connection reader thread. Frames are
@@ -98,7 +94,13 @@
  */
 class Stream<T> extends ExchangeImpl<T> {
 
-    final AsyncDataReadQueue inputQ = new AsyncDataReadQueue();
+    final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+    final System.Logger  debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+    final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
+    final SequentialScheduler sched =
+            new SequentialScheduler(new SynchronizedRestartableTask(this::schedule));
+    final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
 
     /**
      * This stream's identifier. Assigned lazily by the HTTP2Connection before
@@ -117,13 +119,15 @@
     HttpHeadersImpl responseHeaders;
     final HttpHeadersImpl requestHeaders;
     final HttpHeadersImpl requestPseudoHeaders;
-    HttpResponse.BodyProcessor<T> responseProcessor;
-    final HttpRequest.BodyProcessor requestProcessor;
+    volatile HttpResponse.BodySubscriber<T> responseSubscriber;
+    final HttpRequest.BodyPublisher requestPublisher;
+    volatile RequestSubscriber requestSubscriber;
     volatile int responseCode;
     volatile Response response;
     volatile CompletableFuture<Response> responseCF;
-    final AbstractPushPublisher<ByteBuffer> publisher;
+    volatile Throwable failed; // The exception with which this stream was canceled.
     final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
+    volatile CompletableFuture<T> responseBodyCF;
 
     /** True if END_STREAM has been seen in a frame received on this stream. */
     private volatile boolean remotelyClosed;
@@ -146,15 +150,91 @@
         return connection.connection;
     }
 
+    /**
+     * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
+     * of after user subscription window has re-opened, from SubscriptionBase.request()
+     */
+    private void schedule() {
+        if (responseSubscriber == null)
+            // can't process anything yet
+            return;
+
+        while (!inputQ.isEmpty()) {
+            Http2Frame frame  = inputQ.peek();
+            if (frame instanceof ResetFrame) {
+                inputQ.remove();
+                handleReset((ResetFrame)frame);
+                return;
+            }
+            DataFrame df = (DataFrame)frame;
+            boolean finished = df.getFlag(DataFrame.END_STREAM);
+
+            ByteBufferReference[] buffers = df.getData();
+            List<ByteBuffer> dsts = Arrays.stream(buffers)
+                .map(ByteBufferReference::get)
+                .filter(ByteBuffer::hasRemaining)
+                .collect(Collectors.collectingAndThen(toList(), Collections::unmodifiableList));
+            int size = (int)Utils.remaining(dsts);
+            if (size == 0 && finished) {
+                inputQ.remove();
+                Log.logTrace("responseSubscriber.onComplete");
+                debug.log(Level.DEBUG, "incoming: onComplete");
+                sched.stop();
+                responseSubscriber.onComplete();
+                setEndStreamReceived();
+                return;
+            } else if (userSubscription.tryDecrement()) {
+                inputQ.remove();
+                Log.logTrace("responseSubscriber.onNext {0}", size);
+                debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
+                responseSubscriber.onNext(dsts);
+                if (consumed(df)) {
+                    Log.logTrace("responseSubscriber.onComplete");
+                    debug.log(Level.DEBUG, "incoming: onComplete");
+                    sched.stop();
+                    responseSubscriber.onComplete();
+                    setEndStreamReceived();
+                    return;
+                }
+            } else {
+                return;
+            }
+        }
+        Throwable t = failed;
+        if (t != null) {
+            sched.stop();
+            responseSubscriber.onError(t);
+            close();
+        }
+    }
+
+    // Callback invoked after the Response BodyProcessor has consumed the
+    // buffers contained in a DataFrame.
+    // Returns true if END_STREAM is reached, false otherwise.
+    private boolean consumed(DataFrame df) {
+        // RFC 7540 6.1:
+        // The entire DATA frame payload is included in flow control,
+        // including the Pad Length and Padding fields if present
+        int len = df.payloadLength();
+        connection.windowUpdater.update(len);
+
+        if (!df.getFlag(DataFrame.END_STREAM)) {
+            // Don't send window update on a stream which is
+            // closed or half closed.
+            windowUpdater.update(len);
+            return false; // more data coming
+        }
+        return true; // end of stream
+    }
+
     @Override
     CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
                                        boolean returnConnectionToPool,
                                        Executor executor)
     {
         Log.logTrace("Reading body on stream {0}", streamid);
-        responseProcessor = handler.apply(responseCode, responseHeaders);
-        publisher.subscribe(responseProcessor);
-        CompletableFuture<T> cf = receiveData(executor);
+        responseSubscriber = handler.apply(responseCode, responseHeaders);
+        CompletableFuture<T> cf = receiveData();
 
         PushGroup<?,?> pg = exchange.getPushGroup();
         if (pg != null) {
@@ -165,20 +245,6 @@
     }
 
     @Override
-    T readBody(HttpResponse.BodyHandler<T> handler, boolean returnConnectionToPool)
-        throws IOException
-    {
-        CompletableFuture<T> cf = readBodyAsync(handler,
-                                                returnConnectionToPool,
-                                                null);
-        try {
-            return cf.join();
-        } catch (CompletionException e) {
-            throw Utils.getIOException(e);
-        }
-    }
-
-    @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("streamid: ")
@@ -186,67 +252,38 @@
         return sb.toString();
     }
 
-    private boolean receiveDataFrame(Http2Frame frame) throws IOException, InterruptedException {
-        if (frame instanceof ResetFrame) {
-            handleReset((ResetFrame) frame);
-            return true;
-        } else if (!(frame instanceof DataFrame)) {
-            assert false;
-            return true;
-        }
-        DataFrame df = (DataFrame) frame;
-        // RFC 7540 6.1:
-        // The entire DATA frame payload is included in flow control,
-        // including the Pad Length and Padding fields if present
-        int len = df.payloadLength();
-        ByteBufferReference[] buffers = df.getData();
-        for (ByteBufferReference b : buffers) {
-            ByteBuffer buf = b.get();
-            if (buf.hasRemaining()) {
-                publisher.acceptData(Optional.of(buf));
-            }
-        }
-        connection.windowUpdater.update(len);
-        if (df.getFlag(DataFrame.END_STREAM)) {
-            setEndStreamReceived();
-            publisher.acceptData(Optional.empty());
-            return false;
-        }
-        // Don't send window update on a stream which is
-        // closed or half closed.
-        windowUpdater.update(len);
-        return true;
+    private void receiveDataFrame(DataFrame df) {
+        inputQ.add(df);
+        sched.runOrSchedule();
+    }
+
+    /**
+     * RESET always handled inline in queue
+     */
+    private void receiveResetFrame(ResetFrame frame) {
+        inputQ.add(frame);
+        sched.runOrSchedule();
     }
 
-    // pushes entire response body into response processor
+    // pushes entire response body into response subscriber
     // blocking when required by local or remote flow control
-    CompletableFuture<T> receiveData(Executor executor) {
-        CompletableFuture<T> cf = responseProcessor
+    CompletableFuture<T> receiveData() {
+        responseBodyCF = responseSubscriber
                 .getBody()
                 .toCompletableFuture();
-        Consumer<Throwable> onError = e -> {
-            Log.logTrace("receiveData: {0}", e.toString());
-            e.printStackTrace();
-            cf.completeExceptionally(e);
-            publisher.acceptError(e);
-        };
-        if (executor == null) {
-            inputQ.blockingReceive(this::receiveDataFrame, onError);
+
+        if (isCanceled()) {
+            Throwable t = getCancelCause();
+            responseBodyCF.completeExceptionally(t);
+            sched.runOrSchedule();
         } else {
-            inputQ.asyncReceive(executor, this::receiveDataFrame, onError);
+            responseSubscriber.onSubscribe(userSubscription);
+            sched.runOrSchedule(); // in case data waiting already to be processed
         }
-        return cf;
+        return responseBodyCF;
     }
 
     @Override
-    void sendBody() throws IOException {
-        try {
-            sendBodyImpl().join();
-        } catch (CompletionException e) {
-            throw Utils.getIOException(e);
-        }
-    }
-
     CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
         return sendBodyImpl().thenApply( v -> this);
     }
@@ -262,7 +299,7 @@
         this.connection = connection;
         this.windowController = windowController;
         this.request = e.request();
-        this.requestProcessor = request.requestProcessor;
+        this.requestPublisher = request.requestPublisher;  // may be null
         responseHeaders = new HttpHeadersImpl();
         requestHeaders = new HttpHeadersImpl();
         rspHeadersConsumer = (name, value) -> {
@@ -274,7 +311,6 @@
         };
         this.requestPseudoHeaders = new HttpHeadersImpl();
         // NEW
-        this.publisher = new BlockingPushPublisher<>();
         this.windowUpdater = new StreamWindowUpdateSender(connection);
     }
 
@@ -284,17 +320,18 @@
      * Data frames will be removed by response body thread.
      */
     void incoming(Http2Frame frame) throws IOException {
+        debug.log(Level.DEBUG, "incoming: %s", frame);
         if ((frame instanceof HeaderFrame)) {
             HeaderFrame hframe = (HeaderFrame)frame;
             if (hframe.endHeaders()) {
                 Log.logTrace("handling response (streamid={0})", streamid);
                 handleResponse();
                 if (hframe.getFlag(HeaderFrame.END_STREAM)) {
-                    inputQ.put(new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]));
+                    receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]));
                 }
             }
         } else if (frame instanceof DataFrame) {
-            inputQ.put(frame);
+            receiveDataFrame((DataFrame)frame);
         } else {
             otherFrame(frame);
         }
@@ -349,50 +386,32 @@
         completeResponse(response);
     }
 
-    void incoming_reset(ResetFrame frame) throws IOException {
+    void incoming_reset(ResetFrame frame) {
         Log.logTrace("Received RST_STREAM on stream {0}", streamid);
         if (endStreamReceived()) {
             Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
         } else if (closed) {
             Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
         } else {
-            boolean pushedToQueue = false;
-            synchronized(this) {
-                // if the response headers are not yet
-                // received, or the inputQueue is closed, handle reset directly.
-                // Otherwise, put it in the input queue in order to read all
-                // pending data frames first. Indeed, a server may send
-                // RST_STREAM after sending END_STREAM, in which case we should
-                // ignore it. However, we won't know if we have received END_STREAM
-                // or not until all pending data frames are read.
-                // Because the inputQ will not be read until the response
-                // headers are received, and because response headers won't be
-                // sent if the server sent RST_STREAM, then we must handle
-                // reset here directly unless responseHeadersReceived is true.
-                pushedToQueue = !closed && responseHeadersReceived && inputQ.tryPut(frame);
-            }
-            if (!pushedToQueue) {
-                // RST_STREAM was not pushed to the queue: handle it.
-                try {
-                    handleReset(frame);
-                } catch (IOException io) {
-                    completeResponseExceptionally(io);
-                }
-            } else {
-                // RST_STREAM was pushed to the queue. It will be handled by
-                // asyncReceive after all pending data frames have been
-                // processed.
-                Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
-            }
+            // put it in the input queue in order to read all
+            // pending data frames first. Indeed, a server may send
+            // RST_STREAM after sending END_STREAM, in which case we should
+            // ignore it. However, we won't know if we have received END_STREAM
+            // or not until all pending data frames are read.
+            receiveResetFrame(frame);
+            // RST_STREAM was pushed to the queue. It will be handled by
+            // asyncReceive after all pending data frames have been
+            // processed.
+            Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
         }
     }
 
-    void handleReset(ResetFrame frame) throws IOException {
+    void handleReset(ResetFrame frame) {
         Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
         if (!closed) {
             close();
             int error = frame.getErrorCode();
-            throw new IOException(ErrorFrame.stringForCode(error));
+            completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
         } else {
             Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
         }
@@ -431,20 +450,21 @@
         if (pushGroup == null || pushGroup.noMorePushes()) {
             cancelImpl(new IllegalStateException("unexpected push promise"
                 + " on stream " + streamid));
+            return;
         }
 
-        HttpResponse.MultiProcessor<?,T> proc = pushGroup.processor();
+        HttpResponse.MultiSubscriber<?,T> proc = pushGroup.subscriber();
 
         CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
 
-        Optional<HttpResponse.BodyHandler<T>> bpOpt = proc.onRequest(
-                pushReq);
+        Optional<HttpResponse.BodyHandler<T>> bpOpt =
+                pushGroup.handlerForPushRequest(pushReq);
 
         if (!bpOpt.isPresent()) {
             IOException ex = new IOException("Stream "
                  + streamid + " cancelled by user");
             if (Log.trace()) {
-                Log.logTrace("No body processor for {0}: {1}", pushReq,
+                Log.logTrace("No body subscriber for {0}: {1}", pushReq,
                             ex.getMessage());
             }
             pushStream.cancelImpl(ex);
@@ -458,6 +478,7 @@
         // setup housekeeping for when the push is received
         // TODO: deal with ignoring of CF anti-pattern
         cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
+            t = Utils.getCompletionCause(t);
             if (Log.trace()) {
                 Log.logTrace("Push completed on stream {0} for {1}{2}",
                              pushStream.streamid, resp,
@@ -516,34 +537,6 @@
         return requestPseudoHeaders;
     }
 
-    @Override
-    Response getResponse() throws IOException {
-        try {
-            if (request.duration() != null) {
-                Log.logTrace("Waiting for response (streamid={0}, timeout={1}ms)",
-                             streamid,
-                             request.duration().toMillis());
-                return getResponseAsync(null).get(
-                        request.duration().toMillis(), TimeUnit.MILLISECONDS);
-            } else {
-                Log.logTrace("Waiting for response (streamid={0})", streamid);
-                return getResponseAsync(null).join();
-            }
-        } catch (TimeoutException e) {
-            Log.logTrace("Response timeout (streamid={0})", streamid);
-            throw new HttpTimeoutException("Response timed out");
-        } catch (InterruptedException | ExecutionException | CompletionException e) {
-            Throwable t = e.getCause();
-            Log.logTrace("Response failed (streamid={0}): {1}", streamid, t);
-            if (t instanceof IOException) {
-                throw (IOException)t;
-            }
-            throw new IOException(e);
-        } finally {
-            Log.logTrace("Got response or failed (streamid={0})", streamid);
-        }
-    }
-
     /** Sets endStreamReceived. Should be called only once. */
     void setEndStreamReceived() {
         assert remotelyClosed == false: "Unexpected endStream already set";
@@ -558,99 +551,219 @@
     }
 
     @Override
-    void sendHeadersOnly() throws IOException, InterruptedException {
+    CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+        debug.log(Level.DEBUG, "sendHeadersOnly()");
         if (Log.requests() && request != null) {
             Log.logRequest(request.toString());
         }
-        requestContentLen = requestProcessor.contentLength();
+        if (requestPublisher != null) {
+            requestContentLen = requestPublisher.contentLength();
+        } else {
+            requestContentLen = 0;
+        }
         OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
         connection.sendFrame(f);
+        CompletableFuture<ExchangeImpl<T>> cf = new CompletableFuture<ExchangeImpl<T>>();
+        cf.complete(this);  // #### good enough for now
+        return cf;
+    }
+
+    @Override
+    void released() {
+        if (streamid > 0) {
+            debug.log(Level.DEBUG, "Released stream %d", streamid);
+            // remove this stream from the Http2Connection map.
+            connection.closeStream(streamid);
+        } else {
+            debug.log(Level.DEBUG, "Can't release stream %d", streamid);
+        }
+    }
+
+    @Override
+    void completed() {
+        // There should be nothing to do here: the stream should have
+        // been already closed (or will be closed shortly after).
     }
 
     void registerStream(int id) {
         this.streamid = id;
         connection.putStream(this, streamid);
+        debug.log(Level.DEBUG, "Registered stream %d", id);
     }
 
+    void signalWindowUpdate() {
+        RequestSubscriber subscriber = requestSubscriber;
+        assert subscriber != null;
+        debug.log(Level.DEBUG, "Signalling window update");
+        subscriber.sendScheduler.runOrSchedule();
+    }
+
+    static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
     class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
         // can be < 0 if the actual length is not known.
+        private final long contentLength;
         private volatile long remainingContentLength;
         private volatile Subscription subscription;
+        private volatile ByteBuffer current;
+        private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+        // A scheduler used to honor window updates. Writing must be paused
+        // when the window is exhausted, and resumed when the window acquires
+        // some space. The sendScheduler makes it possible to implement this
+        // behaviour in an asynchronous non-blocking way.
+        // See RequestSubscriber::trySend below.
+        final SequentialScheduler sendScheduler;
 
         RequestSubscriber(long contentLen) {
+            this.contentLength = contentLen;
             this.remainingContentLength = contentLen;
+            this.sendScheduler = new SequentialScheduler(
+                    new SynchronizedRestartableTask(this::trySend));
         }
 
         @Override
         public void onSubscribe(Flow.Subscription subscription) {
             if (this.subscription != null) {
-                throw new IllegalStateException();
+                throw new IllegalStateException("already subscribed");
             }
             this.subscription = subscription;
+            debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
             subscription.request(1);
         }
 
         @Override
         public void onNext(ByteBuffer item) {
+            debug.log(Level.DEBUG,
+                    "RequestSubscriber: onNext(%d)", item.remaining());
+            // Got some more request body bytes to send.
             if (requestBodyCF.isDone()) {
-                throw new IllegalStateException();
+                // stream already cancelled, probably in timeout
+                sendScheduler.stop();
+                subscription.cancel();
+                return;
+            }
+            ByteBuffer prev = current;
+            assert prev == null;
+            current = item;
+            sendScheduler.runOrSchedule();
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            debug.log(Level.DEBUG,
+                      () -> "RequestSubscriber: onError: " + throwable);
+            // ensure that errors are handled within the flow.
+            if (errorRef.compareAndSet(null, throwable)) {
+                sendScheduler.runOrSchedule();
             }
+        }
 
+        @Override
+        public void onComplete() {
+            debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
+            // last byte of request body has been obtained.
+            // ensure that everything is completed within the flow.
+            onNext(COMPLETED);
+        }
+
+        // Attempts to send the data, if any.
+        // Handles errors and completion state.
+        // Pause writing if the send window is exhausted, resume it if the
+        // send window has some bytes that can be acquired.
+        void trySend() {
             try {
+                // handle errors raised by onError;
+                Throwable t = errorRef.get();
+                if (t != null) {
+                    sendScheduler.stop();
+                    if (requestBodyCF.isDone()) return;
+                    subscription.cancel();
+                    requestBodyCF.completeExceptionally(t);
+                    return;
+                }
+
+                // handle COMPLETED;
+                ByteBuffer item = current;
+                if (item == null) return;
+                else if (item == COMPLETED) {
+                    sendScheduler.stop();
+                    complete();
+                    return;
+                }
+
+                // handle bytes to send downstream
                 while (item.hasRemaining()) {
+                    debug.log(Level.DEBUG, "trySend: %d", item.remaining());
                     assert !endStreamSent : "internal error, send data after END_STREAM flag";
                     DataFrame df = getDataFrame(item);
-                    if (remainingContentLength > 0) {
+                    if (df == null) {
+                        debug.log(Level.DEBUG, "trySend: can't send yet: %d",
+                                  item.remaining());
+                        return; // the send window is exhausted: come back later
+                    }
+
+                    if (contentLength > 0) {
                         remainingContentLength -= df.getDataLength();
-                        assert remainingContentLength >= 0;
-                        if (remainingContentLength == 0) {
+                        if (remainingContentLength < 0) {
+                            String msg = connection().getConnectionFlow()
+                                         + " stream=" + streamid + " "
+                                         + "[" + Thread.currentThread().getName() +"] "
+                                         + "Too many bytes in request body. Expected: "
+                                         + contentLength + ", got: "
+                                         + (contentLength - remainingContentLength);
+                            connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+                            throw new IOException(msg);
+                        } else if (remainingContentLength == 0) {
                             df.setFlag(DataFrame.END_STREAM);
                             endStreamSent = true;
                         }
                     }
+                    debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
                     connection.sendDataFrame(df);
                 }
+                assert !item.hasRemaining();
+                current = null;
+                debug.log(Level.DEBUG, "trySend: request 1");
                 subscription.request(1);
-            } catch (InterruptedException ex) {
+            } catch (Throwable ex) {
+                debug.log(Level.DEBUG, "trySend: ", ex);
+                sendScheduler.stop();
                 subscription.cancel();
                 requestBodyCF.completeExceptionally(ex);
             }
         }
 
-        @Override
-        public void onError(Throwable throwable) {
-            if (requestBodyCF.isDone()) {
-                return;
+        private void complete() throws IOException {
+            long remaining = remainingContentLength;
+            long written = contentLength - remaining;
+            if (remaining > 0) {
+                connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+                // let trySend() handle the exception
+                throw new IOException(connection().getConnectionFlow()
+                                     + " stream=" + streamid + " "
+                                     + "[" + Thread.currentThread().getName() +"] "
+                                     + "Too few bytes returned by the publisher ("
+                                              + written + "/"
+                                              + contentLength + ")");
             }
-            subscription.cancel();
-            requestBodyCF.completeExceptionally(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            assert endStreamSent || remainingContentLength < 0;
-            try {
-                if (!endStreamSent) {
-                    endStreamSent = true;
-                    connection.sendDataFrame(getEmptyEndStreamDataFrame());
-                }
-                requestBodyCF.complete(null);
-            } catch (InterruptedException ex) {
-                requestBodyCF.completeExceptionally(ex);
+            if (!endStreamSent) {
+                endStreamSent = true;
+                connection.sendDataFrame(getEmptyEndStreamDataFrame());
             }
+            requestBodyCF.complete(null);
         }
     }
 
-    DataFrame getDataFrame(ByteBuffer buffer) throws InterruptedException {
+    DataFrame getDataFrame(ByteBuffer buffer) {
         int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
         // blocks waiting for stream send window, if exhausted
-        int actualAmount = windowController.tryAcquire(requestAmount, streamid);
+        int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
+        if (actualAmount <= 0) return null;
         ByteBuffer outBuf = Utils.slice(buffer,  actualAmount);
         DataFrame df = new DataFrame(streamid, 0 , ByteBufferReference.of(outBuf));
         return df;
     }
 
-    private DataFrame getEmptyEndStreamDataFrame() throws InterruptedException {
+    private DataFrame getEmptyEndStreamDataFrame()  {
         return new DataFrame(streamid, DataFrame.END_STREAM, new ByteBufferReference[0]);
     }
 
@@ -666,7 +779,7 @@
 
     @Override
     CompletableFuture<Response> getResponseAsync(Executor executor) {
-        CompletableFuture<Response> cf = null;
+        CompletableFuture<Response> cf;
         // The code below deals with race condition that can be caused when
         // completeResponse() is being called before getResponseAsync()
         synchronized (response_cfs) {
@@ -693,7 +806,7 @@
         PushGroup<?,?> pg = exchange.getPushGroup();
         if (pg != null) {
             // if an error occurs make sure it is recorded in the PushGroup
-            cf = cf.whenComplete((t,e) -> pg.pushError(e));
+            cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
         }
         return cf;
     }
@@ -763,9 +876,15 @@
     }
 
     CompletableFuture<Void> sendBodyImpl() {
-        RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
-        requestProcessor.subscribe(subscriber);
-        requestBodyCF.whenComplete((v,t) -> requestSent());
+        requestBodyCF.whenComplete((v, t) -> requestSent());
+        if (requestPublisher != null) {
+            final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
+            requestPublisher.subscribe(requestSubscriber = subscriber);
+        } else {
+            // there is no request body, therefore the request is complete,
+            // END_STREAM has already sent with outgoing headers
+            requestBodyCF.complete(null);
+        }
         return requestBodyCF;
     }
 
@@ -787,15 +906,23 @@
         boolean closing;
         if (closing = !closed) { // assigning closing to !closed
             synchronized (this) {
+                failed = e;
                 if (closing = !closed) { // assigning closing to !closed
                     closed=true;
                 }
             }
         }
         if (closing) { // true if the stream has not been closed yet
-            inputQ.close();
+            if (responseSubscriber != null)
+                sched.runOrSchedule();
         }
         completeResponseExceptionally(e);
+        if (!requestBodyCF.isDone()) {
+            requestBodyCF.completeExceptionally(e); // we may be sending the body..
+        }
+        if (responseBodyCF != null) {
+            responseBodyCF.completeExceptionally(e);
+        }
         try {
             // will send a RST_STREAM frame
             if (streamid != 0) {
@@ -814,7 +941,6 @@
             closed = true;
         }
         Log.logTrace("Closing stream {0}", streamid);
-        inputQ.close();
         connection.closeStream(streamid);
         Log.logTrace("Stream {0} closed", streamid);
     }
@@ -860,18 +986,21 @@
         @Override
         CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
             return super.sendBodyAsync()
-                        .whenComplete((ExchangeImpl<T> v, Throwable t) -> pushGroup.pushError(t));
+                        .whenComplete((ExchangeImpl<T> v, Throwable t)
+                                -> pushGroup.pushError(Utils.getCompletionCause(t)));
         }
 
         @Override
         CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
             return super.sendHeadersAsync()
-                        .whenComplete((ExchangeImpl<T> ex, Throwable t) -> pushGroup.pushError(t));
+                        .whenComplete((ExchangeImpl<T> ex, Throwable t)
+                                -> pushGroup.pushError(Utils.getCompletionCause(t)));
         }
 
         @Override
         CompletableFuture<Response> getResponseAsync(Executor executor) {
-            CompletableFuture<Response> cf = pushCF.whenComplete((v, t) -> pushGroup.pushError(t));
+            CompletableFuture<Response> cf = pushCF.whenComplete(
+                    (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
             if(executor!=null && !cf.isDone()) {
                 cf  = cf.thenApplyAsync( r -> r, executor);
             }
@@ -890,7 +1019,7 @@
 
         @Override
         void completeResponse(Response r) {
-            HttpResponseImpl.logResponse(r);
+            Log.logResponse(r::toString);
             pushCF.complete(r); // not strictly required for push API
             // start reading the body using the obtained BodyProcessor
             CompletableFuture<Void> start = new MinimalFuture<>();
@@ -899,8 +1028,9 @@
                     if (t != null) {
                         responseCF.completeExceptionally(t);
                     } else {
-                        HttpResponseImpl<T> response = new HttpResponseImpl<>(r.request, r, body, getExchange());
-                        responseCF.complete(response);
+                        HttpResponseImpl<T> resp =
+                                new HttpResponseImpl<>(r.request, r, body, getExchange());
+                        responseCF.complete(resp);
                     }
                 });
             start.completeAsync(() -> null, getExchange().executor());
@@ -960,4 +1090,23 @@
         }
     }
 
+    /**
+     * Returns true if this exchange was canceled.
+     * @return true if this exchange was canceled.
+     */
+    synchronized boolean isCanceled() {
+        return failed != null;
+    }
+
+    /**
+     * Returns the cause for which this exchange was canceled, if available.
+     * @return the cause for which this exchange was canceled, if available.
+     */
+    synchronized Throwable getCancelCause() {
+        return failed;
+    }
+
+    final String dbgString() {
+        return connection.dbgString() + "/Stream("+streamid+")";
+    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -25,14 +25,19 @@
 
 package jdk.incubator.http;
 
+import java.lang.System.Logger.Level;
+import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
-import java.util.concurrent.locks.Condition;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.concurrent.locks.ReentrantLock;
+import jdk.incubator.http.internal.common.Utils;
 
 /**
- * A Simple blocking Send Window Flow-Controller that is used to control
- * outgoing Connection and Stream flows, per HTTP/2 connection.
+ * A Send Window Flow-Controller that is used to control outgoing Connection
+ * and Stream flows, per HTTP/2 connection.
  *
  * A Http2Connection has its own unique single instance of a WindowController
  * that it shares with its Streams. Each stream must acquire the appropriate
@@ -44,6 +49,10 @@
  */
 final class WindowController {
 
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+    static final System.Logger DEBUG_LOGGER =
+            Utils.getDebugLogger("WindowController"::toString, DEBUG);
+
     /**
      * Default initial connection Flow-Control Send Window size, as per HTTP/2.
      */
@@ -54,11 +63,14 @@
     /** A Map of the active streams, where the key is the stream id, and the
      *  value is the stream's Send Window size, which may be negative. */
     private final Map<Integer,Integer> streams = new HashMap<>();
+    /** A Map of streams awaiting Send Window. The key is the stream id. The
+     * value is a pair of the Stream ( representing the key's stream id ) and
+     * the requested amount of send Window. */
+    private final Map<Integer, Map.Entry<Stream<?>, Integer>> pending
+            = new LinkedHashMap<>();
 
     private final ReentrantLock controllerLock = new ReentrantLock();
 
-    private final Condition notExhausted = controllerLock.newCondition();
-
     /** A Controller with the default initial window size. */
     WindowController() {
         connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
@@ -75,7 +87,8 @@
         try {
             Integer old = streams.put(streamid, initialStreamWindowSize);
             if (old != null)
-                throw new InternalError("Unexpected entry [" + old + "] for streamid: " + streamid);
+                throw new InternalError("Unexpected entry ["
+                        + old + "] for streamid: " + streamid);
         } finally {
             controllerLock.unlock();
         }
@@ -109,28 +122,45 @@
      * 1) the requested amount, 2) the stream's Send Window, and 3) the
      * connection's Send Window.
      *
-     * This method ( currently ) blocks until some positive amount of Send
-     * Window is available.
+     * A negative or zero value is returned if there's no window available.
+     * When the result is negative or zero, this method arranges for the
+     * given stream's {@link Stream#signalWindowUpdate()} method to be invoke at
+     * a later time when the connection and/or stream window's have been
+     * increased. The {@code tryAcquire} method should then be invoked again to
+     * attempt to acquire the available window.
      */
-    int tryAcquire(int requestAmount, int streamid) throws InterruptedException {
+    int tryAcquire(int requestAmount, int streamid, Stream<?> stream) {
         controllerLock.lock();
         try {
-            int x = 0;
-            Integer streamSize = 0;
-            while (x <= 0) {
-                streamSize = streams.get(streamid);
-                if (streamSize == null)
-                    throw new InternalError("Expected entry for streamid: " + streamid);
-                x = Math.min(requestAmount,
+            Integer streamSize = streams.get(streamid);
+            if (streamSize == null)
+                throw new InternalError("Expected entry for streamid: "
+                                        + streamid);
+            int x = Math.min(requestAmount,
                              Math.min(streamSize, connectionWindowSize));
 
-                if (x <= 0)  // stream window size may be negative
-                    notExhausted.await();
+            if (x <= 0)  { // stream window size may be negative
+                DEBUG_LOGGER.log(Level.DEBUG,
+                        "Stream %d requesting %d but only %d available (stream: %d, connection: %d)",
+                      streamid, requestAmount, Math.min(streamSize, connectionWindowSize),
+                      streamSize, connectionWindowSize);
+                // If there's not enough window size available, put the
+                // caller in a pending list.
+                pending.put(streamid, Map.entry(stream, requestAmount));
+                return x;
             }
 
+            // Remove the caller from the pending list ( if was waiting ).
+            pending.remove(streamid);
+
+            // Update window sizes and return the allocated amount to the caller.
             streamSize -= x;
             streams.put(streamid, streamSize);
             connectionWindowSize -= x;
+            DEBUG_LOGGER.log(Level.DEBUG,
+                  "Stream %d amount allocated %d, now %d available (stream: %d, connection: %d)",
+                  streamid, x, Math.min(streamSize, connectionWindowSize),
+                  streamSize, connectionWindowSize);
             return x;
         } finally {
             controllerLock.unlock();
@@ -140,10 +170,15 @@
     /**
      * Increases the Send Window size for the connection.
      *
+     * A number of awaiting requesters, from unfulfilled tryAcquire requests,
+     * may have their stream's {@link Stream#signalWindowUpdate()} method
+     * scheduled to run ( i.e. awake awaiters ).
+     *
      * @return false if, and only if, the addition of the given amount would
      *         cause the Send Window to exceed 2^31-1 (overflow), otherwise true
      */
     boolean increaseConnectionWindow(int amount) {
+        List<Stream<?>> candidates = null;
         controllerLock.lock();
         try {
             int size = connectionWindowSize;
@@ -151,20 +186,54 @@
             if (size < 0)
                 return false;
             connectionWindowSize = size;
-            notExhausted.signalAll();
+            DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
+
+            // Notify waiting streams, until the new increased window size is
+            // effectively exhausted.
+            Iterator<Map.Entry<Integer,Map.Entry<Stream<?>,Integer>>> iter =
+                    pending.entrySet().iterator();
+
+            while (iter.hasNext() && size > 0) {
+                Map.Entry<Integer,Map.Entry<Stream<?>,Integer>> item = iter.next();
+                Integer streamSize = streams.get(item.getKey());
+                if (streamSize == null) {
+                    iter.remove();
+                } else {
+                    Map.Entry<Stream<?>,Integer> e = item.getValue();
+                    int requestedAmount = e.getValue();
+                    // only wakes up the pending streams for which there is
+                    // at least 1 byte of space in both windows
+                    int minAmount = 1;
+                    if (size >= minAmount && streamSize >= minAmount) {
+                        size -= Math.min(streamSize, requestedAmount);
+                        iter.remove();
+                        if (candidates == null)
+                            candidates = new ArrayList<>();
+                        candidates.add(e.getKey());
+                    }
+                }
+            }
         } finally {
             controllerLock.unlock();
         }
+        if (candidates != null) {
+            candidates.forEach(Stream::signalWindowUpdate);
+        }
         return true;
     }
 
     /**
      * Increases the Send Window size for the given stream.
      *
+     * If the given stream is awaiting window size, from an unfulfilled
+     * tryAcquire request, it will have its stream's {@link
+     * Stream#signalWindowUpdate()} method scheduled to run ( i.e. awoken ).
+     *
      * @return false if, and only if, the addition of the given amount would
      *         cause the Send Window to exceed 2^31-1 (overflow), otherwise true
      */
     boolean increaseStreamWindow(int amount, int streamid) {
+        Stream<?> s = null;
         controllerLock.lock();
         try {
             Integer size = streams.get(streamid);
@@ -174,10 +243,27 @@
             if (size < 0)
                 return false;
             streams.put(streamid, size);
-            notExhausted.signalAll();
+            DEBUG_LOGGER.log(Level.DEBUG,
+                             "Stream %s window size is now %s", streamid, size);
+
+            Map.Entry<Stream<?>,Integer> p = pending.get(streamid);
+            if (p != null) {
+                int minAmount = 1;
+                // only wakes up the pending stream if there is at least
+                // 1 byte of space in both windows
+                if (size >= minAmount
+                        && connectionWindowSize >= minAmount) {
+                     pending.remove(streamid);
+                     s = p.getKey();
+                }
+            }
         } finally {
             controllerLock.unlock();
         }
+
+        if (s != null)
+            s.signalWindowUpdate();
+
         return true;
     }
 
@@ -199,6 +285,8 @@
                     Integer size = entry.getValue();
                     size += adjustAmount;
                     streams.put(streamid, size);
+                    DEBUG_LOGGER.log(Level.DEBUG,
+                        "Stream %s window size is now %s", streamid, size);
                 }
             }
         } finally {
@@ -228,4 +316,5 @@
             controllerLock.unlock();;
         }
     }
+
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java	Sun Nov 05 17:32:13 2017 +0000
@@ -24,13 +24,18 @@
  */
 package jdk.incubator.http;
 
+import java.lang.System.Logger.Level;
 import jdk.incubator.http.internal.frame.SettingsFrame;
 import jdk.incubator.http.internal.frame.WindowUpdateFrame;
+import jdk.incubator.http.internal.common.Utils;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
 abstract class WindowUpdateSender {
 
+    final static boolean DEBUG = Utils.DEBUG;
+    final System.Logger debug =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
 
     final int limit;
     final Http2Connection connection;
@@ -59,6 +64,7 @@
     abstract int getStreamId();
 
     void update(int delta) {
+        debug.log(Level.DEBUG, "update: %d", delta);
         if (received.addAndGet(delta) > limit) {
             synchronized (this) {
                 int tosend = received.get();
@@ -71,8 +77,12 @@
     }
 
     void sendWindowUpdate(int delta) {
+        debug.log(Level.DEBUG, "sending window update: %d", delta);
         connection.sendUnorderedFrame(new WindowUpdateFrame(getStreamId(), delta));
     }
 
+    String dbgString() {
+        return "WindowUpdateSender(stream: " + getStreamId() + ")";
+    }
 
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncDataReadQueue.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncDataReadQueue.java	Sun Nov 05 17:32:13 2017 +0000
@@ -126,7 +126,7 @@
             return;
         }
 
-        flushAsync(true);
+        flushAsync(false);
     }
 
     private static <T> boolean checkCanSet(String name, T oldval, Consumer<Throwable> onError) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncWriteQueue.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/AsyncWriteQueue.java	Sun Nov 05 17:32:13 2017 +0000
@@ -44,7 +44,7 @@
          * to the source queue by calling {@code source.setDelayed(buffers)}
          * and return false. If all the data was successfully sent downstream
          * then returns true.
-         * @param buffers An array of ButeBufferReference containing data
+         * @param buffers An array of ByteBufferReference containing data
          *                to send downstream.
          * @param source This AsyncWriteQueue.
          * @return true if all the data could be sent downstream, false otherwise.
@@ -94,7 +94,7 @@
     }
 
     /**
-     * retruns true if flushing was performed
+     * Returns true if flushing was performed
      * @return
      * @throws IOException
      */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/ConnectionExpiredException.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.io.IOException;
+
+/**
+ * Signals that an end of file or end of stream has been reached
+ * unexpectedly before any protocol specific data has been received.
+ */
+public final class ConnectionExpiredException extends IOException {
+    private static final long serialVersionUID = 0;
+
+    /**
+     * Constructs a {@code ConnectionExpiredException} with the specified detail
+     * message.
+     *
+     * @param   s   the detail message
+     */
+    public ConnectionExpiredException(String s) {
+        super(s);
+    }
+
+    /**
+     * Constructs a {@code ConnectionExpiredException} with the specified detail
+     * message and cause.
+     *
+     * @param   s     the detail message
+     * @param   cause the throwable cause
+     */
+    public ConnectionExpiredException(String s, Throwable cause) {
+        super(s, cause);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Demand.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Encapsulates operations with demand (Reactive Streams).
+ *
+ * <p> Demand is the aggregated number of elements requested by a Subscriber
+ * which is yet to be delivered (fulfilled) by the Publisher.
+ */
+public final class Demand {
+
+    private final AtomicLong val = new AtomicLong();
+
+    /**
+     * Increases this demand by the specified positive value.
+     *
+     * @param n
+     *         increment {@code > 0}
+     *
+     * @return {@code true} iff prior to this operation this demand was fulfilled
+     */
+    public boolean increase(long n) {
+        if (n <= 0) {
+            throw new IllegalArgumentException(String.valueOf(n));
+        }
+        long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
+        return prev == 0;
+    }
+
+    /**
+     * Tries to decrease this demand by the specified positive value.
+     *
+     * <p> The actual value this demand has been decreased by might be less than
+     * {@code n}, including {@code 0} (no decrease at all).
+     *
+     * @param n
+     *         decrement {@code > 0}
+     *
+     * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been
+     *         actually decreased by
+     */
+    public long decreaseAndGet(long n) {
+        if (n <= 0) {
+            throw new IllegalArgumentException(String.valueOf(n));
+        }
+        long p, d;
+        do {
+            d = val.get();
+            p = Math.min(d, n);
+        } while (!val.compareAndSet(d, d - p));
+        return p;
+    }
+
+    /**
+     * Tries to decrease this demand by {@code 1}.
+     *
+     * @return {@code true} iff this demand has been decreased by {@code 1}
+     */
+    public boolean tryDecrement() {
+        return decreaseAndGet(1) == 1;
+    }
+
+    /**
+     * @return {@code true} iff there is no unfulfilled demand
+     */
+    public boolean isFulfilled() {
+        return val.get() == 0;
+    }
+
+    /**
+     * Resets this object to its initial state.
+     */
+    public void reset() {
+        val.set(0);
+    }
+
+    /**
+     * Returns the current value of this demand.
+     *
+     * @return the current value of this demand
+     */
+    public long get() {
+        return val.get();
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(val.get());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/FlowTube.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.Flow;
+
+/**
+ * FlowTube is an I/O abstraction that allows reading from and writing to a
+ * destination asynchronously.
+ * This is not a {@link Flow.Processor
+ * Flow.Processor&lt;List&lt;ByteBuffer&gt;, List&lt;ByteBuffer&gt;&gt;},
+ * but rather models a publisher source and a subscriber sink in a bidirectional flow.
+ * <p>
+ * The {@code connectFlows} method should be called to connect the bidirectional
+ * flow. A FlowTube supports handing over the same read subscription to different
+ * sequential read subscribers over time. When {@code connectFlows(writePublisher,
+ * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on
+ * its former readSubscriber, and {@code onConnection} on its new readSubscriber.
+ * By default, the implementation of {@code onConnection} is to call
+ * {@code onSubscribe}, but a subscriber that needs to subscribe sequentially
+ * several times to the same FlowTube may override the default implementation
+ * to ensure that {@code onSubscribe} is called only once.
+ *
+ */
+public interface FlowTube extends
+       Flow.Publisher<List<ByteBuffer>>,
+       Flow.Subscriber<List<ByteBuffer>> {
+
+    /**
+     * A subscriber for reading from the bidirectional flow.
+     * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled
+     * by calling {@code dropSubscription()}.
+     * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+     * should stop calling any method on its subscription.
+     */
+    static interface TubeSubscriber extends Flow.Subscriber<List<ByteBuffer>> {
+        /**
+         * Called by {@code FlowTube.connectFlows}.
+         * @param subscription the subscription.
+         * @implSpec By default this method call {@code this.onSubscribe()}.
+         */
+        default void onConnection(Flow.Subscription subscription) {
+            onSubscribe(subscription);
+        }
+
+        /**
+         * Called when the flow is connected again, and the subscription
+         * is handed over to a new subscriber.
+         * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+         * should stop calling any method on its subscription.
+         */
+        default void dropSubscription() { }
+
+    }
+
+    /**
+     * A publisher for writing to the bidirectional flow.
+     */
+    static interface TubePublisher extends Flow.Publisher<List<ByteBuffer>> {
+
+    }
+
+    /**
+     * Connects the bidirectional flows to a write {@code Publisher} and a
+     * read {@code Subscriber}. This method can be called sequentially
+     * several times to switch existing publishers and subscribers to a new
+     * pair of write subscriber and read publisher.
+     * @param writePublisher A new publisher for writing to the bidirectional flow.
+     * @param readSubscriber A new subscriber for reading from the bidirectional
+     *                       flow.
+     */
+    default void connectFlows(TubePublisher writePublisher,
+                              TubeSubscriber readSubscriber) {
+
+        this.subscribe(readSubscriber);
+        writePublisher.subscribe(this);
+    }
+
+    /**
+     * Returns true if this flow was completed, either exceptionally
+     * or normally (EOF reached).
+     * @return true if the flow is finished
+     */
+    boolean isFinished();
+
+
+    /**
+     * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise
+     * wraps it in a {@code TubeSubscriber}.
+     * Using the wrapper is only appropriate in the case where
+     * {@code dropSubscription} doesn't need to be implemented, and the
+     * {@code TubeSubscriber} is subscribed only once.
+     *
+     * @param s a subscriber for reading.
+     * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a
+     *           {@code TubeSubscriber}, otherwise a {@code TubeSubscriber}
+     *           wrapper that delegates to {@code s}
+     */
+    static TubeSubscriber asTubeSubscriber(Flow.Subscriber<? super List<ByteBuffer>> s) {
+        if (s instanceof TubeSubscriber) {
+            return (TubeSubscriber) s;
+        }
+        return new AbstractTubeSubscriber.TubeSubscriberWrapper(s);
+    }
+
+    /**
+     * Returns {@code s} if {@code s} is a {@code  TubePublisher}, otherwise
+     * wraps it in a {@code  TubePublisher}.
+     *
+     * @param p a publisher for writing.
+     * @return a {@code TubePublisher}: either {@code s} if {@code s} is a
+     *           {@code  TubePublisher}, otherwise a {@code TubePublisher}
+     *           wrapper that delegates to {@code s}
+     */
+    static TubePublisher asTubePublisher(Flow.Publisher<List<ByteBuffer>> p) {
+        if (p instanceof TubePublisher) {
+            return (TubePublisher) p;
+        }
+        return new AbstractTubePublisher.TubePublisherWrapper(p);
+    }
+
+    /**
+     * Convenience abstract class for {@code TubePublisher} implementations.
+     * It is not required that a {@code TubePublisher} implementation extends
+     * this class.
+     */
+    static abstract class AbstractTubePublisher implements TubePublisher {
+        static final class TubePublisherWrapper extends AbstractTubePublisher {
+            final Flow.Publisher<List<ByteBuffer>> delegate;
+            public TubePublisherWrapper(Flow.Publisher<List<ByteBuffer>> delegate) {
+                this.delegate = delegate;
+            }
+            @Override
+            public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+                delegate.subscribe(subscriber);
+            }
+        }
+    }
+
+    /**
+     * Convenience abstract class for {@code TubeSubscriber} implementations.
+     * It is not required that a {@code TubeSubscriber} implementation extends
+     * this class.
+     */
+    static abstract class AbstractTubeSubscriber implements TubeSubscriber {
+        static final class TubeSubscriberWrapper extends  AbstractTubeSubscriber {
+            final Flow.Subscriber<? super List<ByteBuffer>> delegate;
+            TubeSubscriberWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+                this.delegate = delegate;
+            }
+            @Override
+            public void dropSubscription() {}
+            @Override
+            public void onConnection(Flow.Subscription subscription) {
+                delegate.onSubscribe(subscription);
+            }
+            @Override
+            public void onSubscribe(Flow.Subscription subscription) {
+                delegate.onSubscribe(subscription);
+            }
+            @Override
+            public void onNext(List<ByteBuffer> item) {
+                delegate.onNext(item);
+            }
+            @Override
+            public void onError(Throwable throwable) {
+                delegate.onError(throwable);
+            }
+            @Override
+            public void onComplete() {
+                delegate.onComplete();
+            }
+        }
+
+    }
+
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/HttpHeadersImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/HttpHeadersImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -38,7 +38,7 @@
 /**
  * Implementation of HttpHeaders.
  */
-public class HttpHeadersImpl implements HttpHeaders {
+public class HttpHeadersImpl extends HttpHeaders {
 
     private final TreeMap<String,List<String>> headers;
 
@@ -47,17 +47,6 @@
     }
 
     @Override
-    public Optional<String> firstValue(String name) {
-        List<String> l = headers.get(name);
-        return Optional.ofNullable(l == null ? null : l.get(0));
-    }
-
-    @Override
-    public List<String> allValues(String name) {
-        return headers.get(name);
-    }
-
-    @Override
     public Map<String, List<String>> map() {
         return Collections.unmodifiableMap(headers);
     }
@@ -91,17 +80,6 @@
         headers.put(name, values);
     }
 
-    @Override
-    public OptionalLong firstValueAsLong(String name) {
-        List<String> l = headers.get(name);
-        if (l == null) {
-            return OptionalLong.empty();
-        } else {
-            String v = l.get(0);
-            return OptionalLong.of(Long.parseLong(v));
-        }
-    }
-
     public void clear() {
         headers.clear();
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Log.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Log.java	Sun Nov 05 17:32:13 2017 +0000
@@ -27,6 +27,8 @@
 
 import java.io.IOException;
 import jdk.incubator.http.HttpHeaders;
+
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -36,6 +38,9 @@
 import jdk.incubator.http.internal.frame.Http2Frame;
 import jdk.incubator.http.internal.frame.WindowUpdateFrame;
 
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLParameters;
+
 /**
  * -Djava.net.HttpClient.log=
  *          errors,requests,headers,
@@ -196,9 +201,9 @@
         }
     }
 
-    public static void logResponse(String s, Object... s1) {
+    public static void logResponse(Supplier<String> supplier) {
         if (requests()) {
-            logger.log(Level.INFO, "RESPONSE: " + s, s1);
+            logger.log(Level.INFO, "RESPONSE: " + supplier.get());
         }
     }
 
@@ -227,6 +232,55 @@
         }
     }
 
+    public static void logParams(SSLParameters p) {
+        if (!Log.ssl()) {
+            return;
+        }
+
+        if (p == null) {
+            Log.logSSL("SSLParameters: Null params");
+            return;
+        }
+
+        final StringBuilder sb = new StringBuilder("SSLParameters:");
+        final List<Object> params = new ArrayList<>();
+        if (p.getCipherSuites() != null) {
+            for (String cipher : p.getCipherSuites()) {
+                sb.append("\n    cipher: {")
+                        .append(params.size()).append("}");
+                params.add(cipher);
+            }
+        }
+
+        // SSLParameters.getApplicationProtocols() can't return null
+        // JDK 8 EXCL START
+        for (String approto : p.getApplicationProtocols()) {
+            sb.append("\n    application protocol: {")
+                    .append(params.size()).append("}");
+            params.add(approto);
+        }
+        // JDK 8 EXCL END
+
+        if (p.getProtocols() != null) {
+            for (String protocol : p.getProtocols()) {
+                sb.append("\n    protocol: {")
+                        .append(params.size()).append("}");
+                params.add(protocol);
+            }
+        }
+
+        if (p.getServerNames() != null) {
+            for (SNIServerName sname : p.getServerNames()) {
+                sb.append("\n    server name: {")
+                        .append(params.size()).append("}");
+                params.add(sname.toString());
+            }
+        }
+        sb.append('\n');
+
+        Log.logSSL(sb.toString(), params.toArray());
+    }
+
     public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) {
         if (headers != null) {
             Map<String,List<String>> h = headers.map();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/MinimalFuture.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/MinimalFuture.java	Sun Nov 05 17:32:13 2017 +0000
@@ -139,7 +139,7 @@
         }
     }
 
-    public <U> MinimalFuture<U> newIncompleteFuture() {
+    public static <U> MinimalFuture<U> newMinimalFuture() {
         return new MinimalFuture<>();
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLFlowDelegate.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,821 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+
+/**
+ * Implements SSL using two SubscriberWrappers.
+ *
+ * <p> Constructor takes two Flow.Subscribers: one that receives the network
+ * data (after it has been encrypted by SSLFlowDelegate) data, and one that
+ * receives the application data (before it has been encrypted by SSLFlowDelegate).
+ *
+ * <p> Methods upstreamReader() and upstreamWriter() return the corresponding
+ * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data.
+ * See diagram below.
+ *
+ * <p> How Flow.Subscribers are used in this class, and where they come from:
+ * <pre>
+ * {@code
+ *
+ *
+ *
+ * --------->  data flow direction
+ *
+ *
+ *                         +------------------+
+ *        upstreamWriter   |                  | downWriter
+ *        ---------------> |                  | ------------>
+ *  obtained from this     |                  | supplied to constructor
+ *                         | SSLFlowDelegate  |
+ *        downReader       |                  | upstreamReader
+ *        <--------------- |                  | <--------------
+ * supplied to constructor |                  | obtained from this
+ *                         +------------------+
+ * }
+ * </pre>
+ */
+public class SSLFlowDelegate {
+
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    final System.Logger debug =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
+
+    final Executor exec;
+    final Reader reader;
+    final Writer writer;
+    final SSLEngine engine;
+    final String tubeName; // hack
+    final CompletableFuture<Void> cf;
+    final CompletableFuture<String> alpnCF; // completes on initial handshake
+    final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
+
+    /**
+     * Creates an SSLFlowDelegate fed from two Flow.Subscribers. Each
+     * Flow.Subscriber requires an associated {@link CompletableFuture}
+     * for errors that need to be signaled from downstream to upstream.
+     */
+    public SSLFlowDelegate(SSLEngine engine,
+                           Executor exec,
+                           Subscriber<? super List<ByteBuffer>> downReader,
+                           Subscriber<? super List<ByteBuffer>> downWriter)
+    {
+        this.tubeName = String.valueOf(downWriter);
+        this.reader = new Reader();
+        this.reader.subscribe(downReader);
+        this.writer = new Writer();
+        this.writer.subscribe(downWriter);
+        this.engine = engine;
+        this.exec = exec;
+        this.handshakeState = new AtomicInteger(NOT_HANDSHAKING);
+        this.cf = CompletableFuture.allOf(reader.completion(), writer.completion())
+                                   .thenRun(this::normalStop);
+        this.alpnCF = new CompletableFuture<>();
+        //Monitor.add(this::monitor);
+    }
+
+   /**
+    * Returns a CompletableFuture<String> which completes after
+    * the initial handshake completes, and which contains the negotiated
+    * alpn.
+    */
+    public CompletableFuture<String> alpn() {
+        return alpnCF;
+    }
+
+    private void setALPN() {
+        // Handshake is finished. So, can retrieve the ALPN now
+        if (alpnCF.isDone())
+            return;
+        String alpn = engine.getApplicationProtocol();
+        debug.log(Level.DEBUG, "setALPN = %s", alpn);
+        alpnCF.complete(alpn);
+    }
+
+    public String monitor() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("SSL: HS state: " + states(handshakeState));
+        sb.append(" Engine state: " + engine.getHandshakeStatus().toString());
+        sb.append(" LL : ");
+        synchronized(stateList) {
+            for (String s: stateList) {
+                sb.append(s).append(" ");
+            }
+        }
+        sb.append("\r\n");
+        sb.append("Reader:: ").append(reader.toString());
+        sb.append("\r\n");
+        sb.append("Writer:: ").append(writer.toString());
+        sb.append("\r\n===================================");
+        return sb.toString();
+    }
+
+    /**
+     * Processing function for incoming data. Pass it thru SSLEngine.unwrap().
+     * Any decrypted buffers returned to be passed downstream.
+     * Status codes:
+     *     NEED_UNWRAP: do nothing. Following incoming data will contain
+     *                  any required handshake data
+     *     NEED_WRAP: call writer.addData() with empty buffer
+     *     NEED_TASK: delegate task to executor
+     *     BUFFER_OVERFLOW: allocate larger output buffer. Repeat unwrap
+     *     BUFFER_UNDERFLOW: keep buffer and wait for more data
+     *     OK: return generated buffers.
+     *
+     * Upstream subscription strategy is to try and keep no more than
+     * TARGET_BUFSIZE bytes in readBuf
+     */
+    class Reader extends SubscriberWrapper {
+        final SequentialScheduler scheduler;
+        static final int TARGET_BUFSIZE = 16 * 1024;
+        volatile ByteBuffer readBuf;
+        volatile boolean completing = false;
+        final Object readLock = new Object();
+        final System.Logger debugr =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
+
+        class ReaderDownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+            @Override public void run() { processData(); }
+        }
+
+        Reader() {
+            super();
+            scheduler = new SequentialScheduler(new ReaderDownstreamPusher());
+            this.readBuf = ByteBuffer.allocate(1024);
+            readBuf.limit(0); // keep in read mode
+        }
+
+        public final String dbgString() {
+            return "SSL Reader(" + tubeName + ")";
+        }
+
+        /**
+         * entry point for buffers delivered from upstream Subscriber
+         */
+        @Override
+        public void incoming(List<ByteBuffer> buffers, boolean complete) {
+            debugr.log(Level.DEBUG, () -> "Adding " + Utils.remaining(buffers)
+                        + " bytes to read buffer");
+            addToReadBuf(buffers);
+            if (complete) {
+                this.completing = true;
+            }
+            scheduler.runOrSchedule();
+        }
+
+        @Override
+        public String toString() {
+            return "READER: " + super.toString() + " readBuf: " + readBuf.toString()
+                    + " count: " + count.toString();
+        }
+
+        private void reallocReadBuf() {
+            int sz = readBuf.capacity();
+            ByteBuffer newb = ByteBuffer.allocate(sz*2);
+            readBuf.flip();
+            Utils.copy(readBuf, newb);
+            readBuf = newb;
+        }
+
+        @Override
+        protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+            if (readBuf.remaining() > TARGET_BUFSIZE) {
+                return 0;
+            } else {
+                return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
+            }
+        }
+
+        // readBuf is kept ready for reading outside of this method
+        private void addToReadBuf(List<ByteBuffer> buffers) {
+            synchronized (readLock) {
+                for (ByteBuffer buf : buffers) {
+                    readBuf.compact();
+                    while (readBuf.remaining() < buf.remaining())
+                        reallocReadBuf();
+                    readBuf.put(buf);
+                    readBuf.flip();
+                }
+            }
+        }
+
+        void schedule() {
+            scheduler.runOrSchedule();
+        }
+
+        void stop() {
+            debugr.log(Level.DEBUG, "stop");
+            scheduler.stop();
+        }
+
+        AtomicInteger count = new AtomicInteger(0);
+
+        // work function where it all happens
+        void processData() {
+            try {
+                debugr.log(Level.DEBUG, () -> "processData: " + readBuf.remaining()
+                           + " bytes to unwrap "
+                           + states(handshakeState));
+
+                while (readBuf.hasRemaining()) {
+                    boolean handshaking = false;
+                    try {
+                        EngineResult result;
+                        synchronized (readLock) {
+                            result = unwrapBuffer(readBuf);
+                            debugr.log(Level.DEBUG, "Unwrapped: %s", result.result);
+                        }
+                        if (result.status() == Status.BUFFER_UNDERFLOW) {
+                            debugr.log(Level.DEBUG, "BUFFER_UNDERFLOW");
+                            return;
+                        }
+                        if (completing && result.status() == Status.CLOSED) {
+                            debugr.log(Level.DEBUG, "Closed: completing");
+                            outgoing(Utils.EMPTY_BB_LIST, true);
+                            return;
+                        }
+                        if (result.handshaking() && !completing) {
+                            debugr.log(Level.DEBUG, "handshaking");
+                            doHandshake(result, READER);
+                            resumeActivity();
+                            handshaking = true;
+                        } else {
+                            if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
+                                setALPN();
+                                handshaking = false;
+                                resumeActivity();
+                            }
+                        }
+                        if (result.bytesProduced() > 0) {
+                            debugr.log(Level.DEBUG, "sending %d", result.bytesProduced());
+                            count.addAndGet(result.bytesProduced());
+                            outgoing(result.destBuffer, false);
+                        }
+                    } catch (IOException ex) {
+                        errorCommon(ex);
+                        handleError(ex);
+                    }
+                    if (handshaking && !completing)
+                        return;
+                }
+                if (completing) {
+                    debugr.log(Level.DEBUG, "completing");
+                    // Complete the alpnCF, if not already complete, regardless of
+                    // whether or not the ALPN is available, there will be no more
+                    // activity.
+                    setALPN();
+                    outgoing(Utils.EMPTY_BB_LIST, true);
+                }
+            } catch (Throwable ex) {
+                errorCommon(ex);
+                handleError(ex);
+            }
+        }
+    }
+    /**
+     * Returns a CompletableFuture which completes after all activity
+     * in the delegate is terminated (whether normally or exceptionally).
+     *
+     * @return
+     */
+    public CompletableFuture<Void> completion() {
+        return cf;
+    }
+
+    private String xxx(List<ByteBuffer> i) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("xxx size=" + i.size());
+        int x = 0;
+        for (ByteBuffer b : i)
+            x += b.remaining();
+        sb.append(" total " + x);
+        return sb.toString();
+    }
+
+    public interface Monitorable {
+        public String getInfo();
+    }
+
+    public static class Monitor extends Thread {
+        final List<Monitorable> list;
+        static Monitor themon;
+
+        static {
+            themon = new Monitor();
+            themon.start(); // uncomment to enable Monitor
+        }
+
+        Monitor() {
+            super("Monitor");
+            setDaemon(true);
+            list = Collections.synchronizedList(new LinkedList<>());
+        }
+
+        void addTarget(Monitorable o) {
+            list.add(o);
+        }
+
+        public static void add(Monitorable o) {
+            themon.addTarget(o);
+        }
+
+        @Override
+        public void run() {
+            System.out.println("Monitor starting");
+            while (true) {
+                try {Thread.sleep(20*1000); } catch (Exception e) {}
+                synchronized (list) {
+                    for (Monitorable o : list) {
+                        System.out.println(o.getInfo());
+                        System.out.println("-------------------------");
+                    }
+                }
+                System.out.println("--o-o-o-o-o-o-o-o-o-o-o-o-o-o-");
+
+            }
+        }
+    }
+
+    /**
+     * Processing function for outgoing data. Pass it thru SSLEngine.wrap()
+     * Any encrypted buffers generated are passed downstream to be written.
+     * Status codes:
+     *     NEED_UNWRAP: call reader.addData() with empty buffer
+     *     NEED_WRAP: call addData() with empty buffer
+     *     NEED_TASK: delegate task to executor
+     *     BUFFER_OVERFLOW: allocate larger output buffer. Repeat wrap
+     *     BUFFER_UNDERFLOW: shouldn't happen on writing side
+     *     OK: return generated buffers
+     */
+    class Writer extends SubscriberWrapper {
+        final SequentialScheduler scheduler;
+        // queues of buffers received from upstream waiting
+        // to be processed by the SSLEngine
+        final List<ByteBuffer> writeList;
+        final System.Logger debugw =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
+
+        class WriterDownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+            @Override public void run() { processData(); }
+        }
+
+        Writer() {
+            super();
+            writeList = Collections.synchronizedList(new LinkedList<>());
+            scheduler = new SequentialScheduler(new WriterDownstreamPusher());
+        }
+
+        @Override
+        protected void incoming(List<ByteBuffer> buffers, boolean complete) {
+            assert complete ? buffers ==  Utils.EMPTY_BB_LIST : true;
+            assert buffers != Utils.EMPTY_BB_LIST ? complete == false : true;
+            if (complete) {
+                writeList.add(SENTINEL);
+            } else {
+                writeList.addAll(buffers);
+            }
+            debugw.log(Level.DEBUG, () -> "added " + buffers.size()
+                        + " (" + Utils.remaining(buffers)
+                        + " bytes) to the writeList");
+            scheduler.runOrSchedule();
+        }
+
+        public final String dbgString() {
+            return "SSL Writer(" + tubeName + ")";
+        }
+
+        protected void onSubscribe() {
+            doHandshake(EngineResult.INIT, INIT);
+            resumeActivity();
+        }
+
+        void schedule() {
+            scheduler.runOrSchedule();
+        }
+
+        void stop() {
+            debugw.log(Level.DEBUG, "stop");
+            scheduler.stop();
+        }
+
+        private boolean isCompleting() {
+            synchronized(writeList) {
+                int lastIndex = writeList.size() - 1;
+                if (lastIndex < 0)
+                    return false;
+                return writeList.get(lastIndex) == SENTINEL;
+            }
+        }
+
+        @Override
+        protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+            if (writeList.size() > 10)
+                return 0;
+            else
+                return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
+        }
+
+        private boolean hsTriggered() {
+            synchronized(writeList) {
+                for (ByteBuffer b : writeList)
+                    if (b == HS_TRIGGER)
+                        return true;
+                return false;
+            }
+        }
+
+        private void processData() {
+            boolean completing = isCompleting();
+
+            try {
+                debugw.log(Level.DEBUG, () -> "processData(" + Utils.remaining(writeList) + ")");
+                while (Utils.remaining(writeList) > 0 || hsTriggered()) {
+                    ByteBuffer[] outbufs = writeList.toArray(Utils.EMPTY_BB_ARRAY);
+                    EngineResult result = wrapBuffers(outbufs);
+                    debugw.log(Level.DEBUG, "wrapBuffer returned %s", result.result);
+
+                    if (result.status() == Status.CLOSED) {
+                        if (result.bytesProduced() <= 0)
+                            return;
+
+                        completing = true;
+                        // There could still be some outgoing data in outbufs.
+                        writeList.add(SENTINEL);
+                    }
+
+                    boolean handshaking = false;
+                    if (result.handshaking()) {
+                        debugw.log(Level.DEBUG, "handshaking");
+                        doHandshake(result, WRITER);
+                        handshaking = true;
+                    } else {
+                        if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
+                            setALPN();
+                            resumeActivity();
+                        }
+                    }
+                    cleanList(writeList); // tidy up the source list
+                    sendResultBytes(result);
+                    if (handshaking && !completing) {
+                        if (writeList.isEmpty() && !result.needUnwrap()) {
+                            writer.addData(HS_TRIGGER);
+                        }
+                        return;
+                    }
+                }
+                if (completing && Utils.remaining(writeList) == 0) {
+                    /*
+                    System.out.println("WRITER DOO 3");
+                    engine.closeOutbound();
+                    EngineResult result = wrapBuffers(Utils.EMPTY_BB_ARRAY);
+                    sendResultBytes(result);
+                    */
+                    outgoing(Utils.EMPTY_BB_LIST, true);
+                    return;
+                }
+            } catch (Throwable ex) {
+                handleError(ex);
+            }
+        }
+
+        private void sendResultBytes(EngineResult result) {
+            if (result.bytesProduced() > 0) {
+                debugw.log(Level.DEBUG, "Sending %d bytes downstream",
+                           result.bytesProduced());
+                outgoing(result.destBuffer, false);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "WRITER: " + super.toString() +
+                    " writeList size " + Integer.toString(writeList.size());
+                    //" writeList: " + writeList.toString();
+        }
+    }
+
+    private void handleError(Throwable t) {
+        debug.log(Level.DEBUG, "handleError", t);
+        cf.completeExceptionally(t);
+        // no-op if already completed
+        alpnCF.completeExceptionally(t);
+        reader.stop();
+        writer.stop();
+    }
+
+    private void normalStop() {
+        reader.stop();
+        writer.stop();
+    }
+
+    private void cleanList(List<ByteBuffer> l) {
+        synchronized (l) {
+            Iterator<ByteBuffer> iter = l.iterator();
+            while (iter.hasNext()) {
+                ByteBuffer b = iter.next();
+                if (!b.hasRemaining()) {
+                    iter.remove();
+                }
+            }
+        }
+    }
+
+    /**
+     * States for handshake. We avoid races when accessing/updating the AtomicInt
+     * because updates always schedule an additional call to both the read()
+     * and write() functions.
+     */
+    private static final int NOT_HANDSHAKING = 0;
+    private static final int HANDSHAKING = 1;
+    private static final int INIT = 2;
+    private static final int DOING_TASKS = 4; // bit added to above state
+    private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
+
+    private static final int READER = 1;
+    private static final int WRITER = 2;
+
+    private static String states(AtomicInteger state) {
+        int s = state.get();
+        StringBuilder sb = new StringBuilder();
+        int x = s & ~DOING_TASKS;
+        switch (x) {
+            case NOT_HANDSHAKING:
+                sb.append(" NOT_HANDSHAKING ");
+                break;
+            case HANDSHAKING:
+                sb.append(" HANDSHAKING ");
+                break;
+            case INIT:
+                sb.append(" INIT ");
+                break;
+            default:
+                throw new InternalError();
+        }
+        if ((s & DOING_TASKS) > 0)
+            sb.append("|DOING_TASKS");
+        return sb.toString();
+    }
+
+    private void resumeActivity() {
+        reader.schedule();
+        writer.schedule();
+    }
+
+    final AtomicInteger handshakeState;
+    final ConcurrentLinkedQueue<String> stateList = new ConcurrentLinkedQueue<>();
+
+    private void doHandshake(EngineResult r, int caller) {
+        int s = handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS));
+        stateList.add(r.handshakeStatus().toString());
+        stateList.add(Integer.toString(caller));
+        switch (r.handshakeStatus()) {
+            case NEED_TASK:
+                if ((s & DOING_TASKS) > 0) // someone else was doing tasks
+                    return;
+                List<Runnable> tasks = obtainTasks();
+                executeTasks(tasks);
+                break;
+            case NEED_WRAP:
+                writer.addData(HS_TRIGGER);
+                break;
+            case NEED_UNWRAP:
+            case NEED_UNWRAP_AGAIN:
+                // do nothing else
+                break;
+            default:
+                throw new InternalError("Unexpected handshake status:"
+                                        + r.handshakeStatus());
+        }
+    }
+
+    private List<Runnable> obtainTasks() {
+        List<Runnable> l = new ArrayList<>();
+        Runnable r;
+        while ((r = engine.getDelegatedTask()) != null) {
+            l.add(r);
+        }
+        return l;
+    }
+
+    private void executeTasks(List<Runnable> tasks) {
+        exec.execute(() -> {
+            handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
+            try {
+                tasks.forEach((r) -> {
+                    r.run();
+                });
+            } catch (Throwable t) {
+                handleError(t);
+            }
+            handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
+            writer.addData(HS_TRIGGER);
+            resumeActivity();
+        });
+    }
+
+
+    EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
+        ByteBuffer dst = getAppBuffer();
+        while (true) {
+            SSLEngineResult sslResult = engine.unwrap(src, dst);
+            switch (sslResult.getStatus()) {
+                case BUFFER_OVERFLOW:
+                    // may happen only if app size buffer was changed.
+                    // get it again if app buffer size changed
+                    int appSize = engine.getSession().getApplicationBufferSize();
+                    ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
+                    dst.flip();
+                    b.put(dst);
+                    dst = b;
+                    break;
+                case CLOSED:
+                    doClosure();
+                    return new EngineResult(sslResult);
+                case BUFFER_UNDERFLOW:
+                    // handled implicitly by compaction/reallocation of readBuf
+                    return new EngineResult(sslResult);
+                case OK:
+                     dst.flip();
+                     return new EngineResult(sslResult, dst);
+            }
+        }
+    }
+
+    // TODO: acknowledge a received CLOSE request from peer
+    void doClosure() throws IOException {
+        //while (!wrapAndSend(emptyArray))
+            //;
+    }
+
+    /**
+     * Returns the upstream Flow.Subscriber of the reading (incoming) side.
+     * This flow must be given the encrypted data read from upstream (eg socket)
+     * before it is decrypted.
+     */
+    public Flow.Subscriber<List<ByteBuffer>> upstreamReader() {
+        return reader;
+    }
+
+    /**
+     * Returns the upstream Flow.Subscriber of the writing (outgoing) side.
+     * This flow contains the plaintext data before it is encrypted.
+     */
+    public Flow.Subscriber<List<ByteBuffer>> upstreamWriter() {
+        return writer;
+    }
+
+    public void resumeReader() {
+        reader.schedule();
+    }
+
+    public void resetReaderDemand() {
+        reader.resetDownstreamDemand();
+    }
+
+    static class EngineResult {
+        final SSLEngineResult result;
+        final ByteBuffer destBuffer;
+
+        // normal result
+        EngineResult(SSLEngineResult result) {
+            this(result, null);
+        }
+
+        EngineResult(SSLEngineResult result, ByteBuffer destBuffer) {
+            this.result = result;
+            this.destBuffer = destBuffer;
+        }
+
+        // Special result used to trigger handshaking in constructor
+        static EngineResult INIT =
+            new EngineResult(
+                new SSLEngineResult(SSLEngineResult.Status.OK, HandshakeStatus.NEED_WRAP, 0, 0));
+
+        boolean handshaking() {
+            HandshakeStatus s = result.getHandshakeStatus();
+            return s != HandshakeStatus.FINISHED
+                   && s != HandshakeStatus.NOT_HANDSHAKING
+                   && result.getStatus() != Status.CLOSED;
+        }
+
+        boolean needUnwrap() {
+            HandshakeStatus s = result.getHandshakeStatus();
+            return s == HandshakeStatus.NEED_UNWRAP;
+        }
+
+
+        int bytesConsumed() {
+            return result.bytesConsumed();
+        }
+
+        int bytesProduced() {
+            return result.bytesProduced();
+        }
+
+        SSLEngineResult.HandshakeStatus handshakeStatus() {
+            return result.getHandshakeStatus();
+        }
+
+        SSLEngineResult.Status status() {
+            return result.getStatus();
+        }
+    }
+
+    public ByteBuffer getNetBuffer() {
+        return ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
+    }
+
+    private ByteBuffer getAppBuffer() {
+        return ByteBuffer.allocate(engine.getSession().getApplicationBufferSize());
+    }
+
+    final String dbgString() {
+        return "SSLFlowDelegate(" + tubeName + ")";
+    }
+
+    @SuppressWarnings("fallthrough")
+    EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
+        debug.log(Level.DEBUG, () -> "wrapping "
+                    + Utils.remaining(src) + " bytes");
+        ByteBuffer dst = getNetBuffer();
+        while (true) {
+            SSLEngineResult sslResult = engine.wrap(src, dst);
+            debug.log(Level.DEBUG, () -> "SSLResult: " + sslResult);
+            switch (sslResult.getStatus()) {
+                case BUFFER_OVERFLOW:
+                    // Shouldn't happen. We allocated buffer with packet size
+                    // get it again if net buffer size was changed
+                    debug.log(Level.DEBUG, "BUFFER_OVERFLOW");
+                    int appSize = engine.getSession().getApplicationBufferSize();
+                    ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
+                    dst.flip();
+                    b.put(dst);
+                    dst = b;
+                    break; // try again
+                case CLOSED:
+                    debug.log(Level.DEBUG, "CLOSED");
+                    // fallthrough. There could be some remaining data in dst.
+                    // CLOSED will be handled by the caller.
+                case OK:
+                    dst.flip();
+                    final ByteBuffer dest = dst;
+                    debug.log(Level.DEBUG, () -> "OK => produced: "
+                                           + dest.remaining()
+                                           + " not wrapped: "
+                                           + Utils.remaining(src));
+                    return new EngineResult(sslResult, dest);
+                case BUFFER_UNDERFLOW:
+                    // Shouldn't happen.  Doesn't returns when wrap()
+                    // underflow handled externally
+                    // assert false : "Buffer Underflow";
+                    debug.log(Level.DEBUG, "BUFFER_UNDERFLOW");
+                    return new EngineResult(sslResult);
+                default:
+                    debug.log(Level.DEBUG, "ASSERT");
+                    assert false;
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SSLTube.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,437 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
+
+public class SSLTube implements FlowTube {
+
+    static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag.
+    final System.Logger debug =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
+
+    private final FlowTube tube;
+    private final SSLSubscriberWrapper readSubscriber;
+    private final SSLSubscriptionWrapper writeSubscription;
+    private final SSLFlowDelegate sslDelegate;
+    private final SSLEngine engine;
+    private volatile boolean finished;
+
+    public SSLTube(SSLEngine engine, Executor executor, FlowTube tube) {
+        Objects.requireNonNull(engine);
+        Objects.requireNonNull(executor);
+        this.tube = Objects.requireNonNull(tube);
+        writeSubscription = new SSLSubscriptionWrapper();
+        readSubscriber = new SSLSubscriberWrapper();
+        this.engine = engine;
+        sslDelegate = new SSLFlowDelegate(engine,
+                                          executor,
+                                          readSubscriber,
+                                          tube); // FIXME
+        tube.subscribe(sslDelegate.upstreamReader());
+        sslDelegate.upstreamWriter().onSubscribe(writeSubscription);
+    }
+
+    public CompletableFuture<String> getALPN() {
+        return sslDelegate.alpn();
+    }
+
+    @Override
+    public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+        readSubscriber.dropSubscription();
+        readSubscriber.setDelegate(s);
+        s.onSubscribe(readSubscription);
+    }
+
+    /**
+     * Tells whether, or not, this FlowTube has finished receiving data.
+     *
+     * @return true when one of this FlowTube Subscriber's OnError or onComplete
+     * methods have been invoked
+     */
+    @Override
+    public boolean isFinished() {
+        return finished;
+    }
+
+    private volatile Flow.Subscription readSubscription;
+
+    // The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and
+    // tracks the subscriber's state. In particular it makes sure that
+    // onComplete/onError are not called before onSubscribed.
+    final static class DelegateWrapper implements FlowTube.TubeSubscriber {
+        private final FlowTube.TubeSubscriber delegate;
+        volatile boolean subscribedCalled;
+        volatile boolean subscribedDone;
+        volatile boolean completed;
+        volatile Throwable error;
+        DelegateWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+            this.delegate = FlowTube.asTubeSubscriber(delegate);
+        }
+
+        @Override
+        public void dropSubscription() {
+            if (subscribedCalled) {
+                delegate.dropSubscription();
+            }
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            assert subscribedCalled;
+            delegate.onNext(item);
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            onSubscribe(delegate::onSubscribe, subscription);
+        }
+
+        @Override
+        public void onConnection(Flow.Subscription subscription) {
+            onSubscribe(delegate::onConnection, subscription);
+        }
+
+        private void onSubscribe(Consumer<Flow.Subscription> method,
+                                 Flow.Subscription subscription) {
+            subscribedCalled = true;
+            method.accept(subscription);
+            Throwable x;
+            boolean finished;
+            synchronized (this) {
+                subscribedDone = true;
+                x = error;
+                finished = completed;
+            }
+            if (x != null) {
+                delegate.onError(x);
+            } else if (finished) {
+                delegate.onComplete();
+            }
+        }
+
+        @Override
+        public void onError(Throwable t) {
+            if (completed) return;
+            boolean subscribed;
+            synchronized (this) {
+                if (completed) return;
+                error = t;
+                completed = true;
+                subscribed = subscribedDone;
+            }
+            if (subscribed) {
+                delegate.onError(t);
+            }
+        }
+
+        @Override
+        public void onComplete() {
+            if (completed) return;
+            boolean subscribed;
+            synchronized (this) {
+                if (completed) return;
+                completed = true;
+                subscribed = subscribedDone;
+            }
+            if (subscribed) {
+                delegate.onComplete();
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DelegateWrapper:" + delegate.toString();
+        }
+
+    }
+
+    // Used to read data from the SSLTube.
+    final class SSLSubscriberWrapper implements FlowTube.TubeSubscriber {
+        private volatile DelegateWrapper delegate;
+        private volatile DelegateWrapper subscribed;
+        private volatile boolean onCompleteReceived;
+        private final AtomicReference<Throwable> errorRef
+                = new AtomicReference<>();
+
+        void setDelegate(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+            debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) got delegate: %s",
+                      delegate);
+            assert delegate != null;
+            DelegateWrapper delegateWrapper = new DelegateWrapper(delegate);
+            Flow.Subscription subscription;
+            synchronized (this) {
+                this.delegate = delegateWrapper;
+                subscription = readSubscription;
+            }
+            if (subscription == null) {
+                debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) no subscription yet");
+                return;
+            }
+
+            onNewSubscription(delegateWrapper,
+                              delegateWrapper::onSubscribe,
+                              subscription);
+        }
+
+        @Override
+        public void dropSubscription() {
+            DelegateWrapper subscriberImpl = delegate;
+            if (subscriberImpl != null) {
+                subscriberImpl.dropSubscription();
+            }
+        }
+
+        @Override
+        public void onConnection(Flow.Subscription subscription) {
+            debug.log(Level.DEBUG,
+                      "SSLSubscriberWrapper (reader) onConnection(%s)",
+                      subscription);
+            assert subscription != null;
+            DelegateWrapper subscriberImpl;
+            synchronized (this) {
+                subscriberImpl = delegate;
+                readSubscription = subscription;
+            }
+            if (subscriberImpl == null) {
+                debug.log(Level.DEBUG,
+                      "SSLSubscriberWrapper (reader) onConnection: no delegate yet");
+                return;
+            }
+            onNewSubscription(subscriberImpl,
+                              subscriberImpl::onConnection,
+                              subscription);
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            debug.log(Level.DEBUG,
+                      "SSLSubscriberWrapper (reader) onSubscribe(%s)",
+                      subscription);
+            readSubscription = subscription;
+            assert subscription != null;
+            DelegateWrapper subscriberImpl;
+            synchronized (this) {
+                subscriberImpl = delegate;
+                readSubscription = subscription;
+            }
+            if (subscriberImpl == null) {
+                debug.log(Level.DEBUG,
+                      "SSLSubscriberWrapper (reader) onSubscribe: no delegate yet");
+                return;
+            }
+            onNewSubscription(subscriberImpl,
+                              subscriberImpl::onSubscribe,
+                              subscription);
+        }
+
+        private void onNewSubscription(DelegateWrapper subscriberImpl,
+                                       Consumer<Flow.Subscription> method,
+                                       Flow.Subscription subscription) {
+            assert subscriberImpl != null;
+            assert method != null;
+            assert subscription != null;
+
+            Throwable failed;
+            boolean completed;
+            // reset any demand that may have been made by the previous
+            // subscriber
+            sslDelegate.resetReaderDemand();
+            // send the subscription to the subscriber.
+            method.accept(subscription);
+            // reschedule after calling onSubscribe (this should not be
+            // strictly needed as the first call to subscription.request()
+            // coming after resetting the demand should trigger it).
+            // However, it should not do any harm.
+            sslDelegate.resumeReader();
+
+            // The following twisted logic is just here that we don't invoke
+            // onError before onSubscribe. It also prevents race conditions
+            // if onError is invoked concurrently with setDelegate.
+            synchronized (this) {
+                failed = this.errorRef.get();
+                completed = finished;
+                if (delegate == subscriberImpl) {
+                    subscribed = subscriberImpl;
+                }
+            }
+            if (failed != null) {
+                subscriberImpl.onError(failed);
+            } else if (completed) {
+                subscriberImpl.onComplete();
+            }
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            delegate.onNext(item);
+        }
+
+        public void onErrorImpl(Throwable throwable) {
+            // The following twisted logic is just here that we don't invoke
+            // onError before onSubscribe. It also prevents race conditions
+            // if onError is invoked concurrently with setDelegate.
+            // See setDelegate.
+
+            errorRef.compareAndSet(null, throwable);
+            Throwable failed = errorRef.get();
+            finished = true;
+            debug.log(Level.DEBUG, "%s: onErrorImpl: %s", this, throwable);
+            DelegateWrapper subscriberImpl;
+            synchronized (this) {
+                subscriberImpl = subscribed;
+            }
+            if (subscriberImpl != null) {
+                subscriberImpl.onError(failed);
+            } else {
+                debug.log(Level.DEBUG, "%s: delegate null, stored %s", this, failed);
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            assert !finished && !onCompleteReceived;
+            onErrorImpl(throwable);
+        }
+
+        private boolean handshaking() {
+            HandshakeStatus hs = engine.getHandshakeStatus();
+            return !(hs == NOT_HANDSHAKING || hs == FINISHED);
+        }
+
+        @Override
+        public void onComplete() {
+            assert !finished && !onCompleteReceived;
+            onCompleteReceived = true;
+            DelegateWrapper subscriberImpl;
+            synchronized(this) {
+                subscriberImpl = subscribed;
+            }
+
+            if (handshaking()) {
+                onErrorImpl(new SSLHandshakeException(
+                        "Remote host terminated the handshake"));
+            } else if (subscriberImpl != null) {
+                finished = true;
+                subscriberImpl.onComplete();
+            }
+        }
+    }
+
+    @Override
+    public void connectFlows(TubePublisher writePub,
+                             TubeSubscriber readSub) {
+        debug.log(Level.DEBUG, "connecting flows");
+        readSubscriber.setDelegate(readSub);
+        writePub.subscribe(this);
+    }
+
+    /** Outstanding write demand from the SSL Flow Delegate. */
+    private final Demand writeDemand = new Demand();
+
+    final class SSLSubscriptionWrapper implements Flow.Subscription {
+
+        volatile Flow.Subscription delegate;
+
+        void setSubscription(Flow.Subscription sub) {
+            long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
+            delegate = sub;
+            debug.log(Level.DEBUG, "setSubscription: demand=%d", demand);
+            if (demand > 0)
+                sub.request(demand);
+        }
+
+        @Override
+        public void request(long n) {
+            writeDemand.increase(n);
+            debug.log(Level.DEBUG, "request: n=%d", n);
+            Flow.Subscription sub = delegate;
+            if (sub != null && n > 0) {
+                sub.request(n);
+            }
+        }
+
+        @Override
+        public void cancel() {
+            // TODO:  no-op or error?
+        }
+    }
+
+    /* Subscriber - writing side */
+    @Override
+    public void onSubscribe(Flow.Subscription subscription) {
+        Objects.requireNonNull(subscription);
+        Flow.Subscription x = writeSubscription.delegate;
+        if (x != null)
+            x.cancel();
+
+        writeSubscription.setSubscription(subscription);
+    }
+
+    @Override
+    public void onNext(List<ByteBuffer> item) {
+        Objects.requireNonNull(item);
+        boolean decremented = writeDemand.tryDecrement();
+        assert decremented : "Unexpected writeDemand: ";
+        debug.log(Level.DEBUG,
+                "sending %d  buffers to SSL flow delegate", item.size());
+        sslDelegate.upstreamWriter().onNext(item);
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+        Objects.requireNonNull(throwable);
+        sslDelegate.upstreamWriter().onError(throwable);
+    }
+
+    @Override
+    public void onComplete() {
+        sslDelegate.upstreamWriter().onComplete();
+    }
+
+    @Override
+    public String toString() {
+        return dbgString();
+    }
+
+    final String dbgString() {
+        return "SSLTube(" + tube + ")";
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SequentialScheduler.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2016, 2017, 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.incubator.http.internal.common;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A scheduler of ( repeatable ) tasks that MUST be run sequentially.
+ *
+ * <p> This class can be used as a synchronization aid that assists a number of
+ * parties in running a task in a mutually exclusive fashion.
+ *
+ * <p> To run the task, a party invokes {@code runOrSchedule}. To permanently
+ * prevent the task from subsequent runs, the party invokes {@code stop}.
+ *
+ * <p> The parties can, but do not have to, operate in different threads.
+ *
+ * <p> The task can be either synchronous ( completes when its {@code run}
+ * method returns ), or asynchronous ( completed when its
+ * {@code DeferredCompleter} is explicitly completed ).
+ *
+ * <p> The next run of the task will not begin until the previous run has
+ * finished.
+ *
+ * <p> The task may invoke {@code runOrSchedule} itself, which may be a normal
+ * situation.
+ */
+public final class SequentialScheduler {
+
+    /*
+       Since the task is fixed and known beforehand, no blocking synchronization
+       (locks, queues, etc.) is required. The job can be done solely using
+       nonblocking primitives.
+
+       The machinery below addresses two problems:
+
+         1. Running the task in a sequential order (no concurrent runs):
+
+                begin, end, begin, end...
+
+         2. Avoiding indefinite recursion:
+
+                begin
+                  end
+                    begin
+                      end
+                        ...
+
+       Problem #1 is solved with a finite state machine with 4 states:
+
+           BEGIN, AGAIN, END, and STOP.
+
+       Problem #2 is solved with a "state modifier" OFFLOAD.
+
+       Parties invoke `runOrSchedule()` to signal the task must run. A party
+       that has invoked `runOrSchedule()` either begins the task or exploits the
+       party that is either beginning the task or ending it.
+
+       The party that is trying to end the task either ends it or begins it
+       again.
+
+       To avoid indefinite recursion, before re-running the task the
+       TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child"
+       TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is
+       available and the "child" must offload the task on to the "parent". Then
+       a race begins. Whichever invocation of TryEndDeferredCompleter.complete
+       manages to unset OFFLOAD bit first does not do the work.
+
+       There is at most 1 thread that is beginning the task and at most 2
+       threads that are trying to end it: "parent" and "child". In case of a
+       synchronous task "parent" and "child" are the same thread.
+     */
+
+    /**
+     * An interface to signal the completion of a {@link RestartableTask}.
+     *
+     * <p> The invocation of {@code complete} completes the task. The invocation
+     * of {@code complete} may restart the task, if an attempt has previously
+     * been made to run the task while it was already running.
+     *
+     * @apiNote {@code DeferredCompleter} is useful when a task is not necessary
+     * complete when its {@code run} method returns, but will complete at a
+     * later time, and maybe in different thread. This type exists for
+     * readability purposes at use-sites only.
+     */
+    public static abstract class DeferredCompleter {
+
+        /** Extensible from this (outer) class ONLY. */
+        private DeferredCompleter() { }
+
+        /** Completes the task. Must be called once, and once only. */
+        public abstract void complete();
+    }
+
+    /**
+     * A restartable task.
+     */
+    @FunctionalInterface
+    public interface RestartableTask {
+
+        /**
+         * The body of the task.
+         *
+         * @param taskCompleter
+         *         A completer that must be invoked once, and only once,
+         *         when this task is logically finished
+         */
+        void run(DeferredCompleter taskCompleter);
+    }
+
+    /**
+     * A complete restartable task is one which is simple and self-contained.
+     * It completes once its {@code run} method returns.
+     */
+    public static abstract class CompleteRestartableTask
+        implements RestartableTask
+    {
+        @Override
+        public final void run(DeferredCompleter taskCompleter) {
+            try {
+                run();
+            } finally {
+                taskCompleter.complete();
+            }
+        }
+
+        /** The body of the task. */
+        protected abstract void run();
+    }
+
+    /**
+     * A RestartableTask that runs its main loop within a
+     * synchronized block to place a memory barrier around it.
+     * Because the main loop can't run concurrently in two treads,
+     * then the lock shouldn't be contended and no deadlock should
+     * ever be possible.
+     */
+    public static final class SynchronizedRestartableTask
+            extends CompleteRestartableTask {
+        private final Runnable mainLoop;
+        private final Object lock = new Object();
+        public SynchronizedRestartableTask(Runnable mainLoop) {
+            this.mainLoop = mainLoop;
+        }
+
+        @Override
+        protected void run() {
+            synchronized(lock) {
+                mainLoop.run();
+            }
+        }
+    }
+
+    private static final int OFFLOAD =  1;
+    private static final int AGAIN   =  2;
+    private static final int BEGIN   =  4;
+    private static final int STOP    =  8;
+    private static final int END     = 16;
+
+    private final AtomicInteger state = new AtomicInteger(END);
+    private final RestartableTask restartableTask;
+    private final DeferredCompleter completer;
+    private final SchedulableTask schedulableTask;
+
+    /**
+     * A simple task that can be pushed on an executor to execute
+     * {@code restartableTask.run(completer)}.
+     */
+    private final class SchedulableTask implements Runnable {
+        @Override
+        public void run() {
+            restartableTask.run(completer);
+        }
+    }
+
+    public SequentialScheduler(RestartableTask restartableTask) {
+        this.restartableTask = requireNonNull(restartableTask);
+        this.completer = new TryEndDeferredCompleter();
+        this.schedulableTask = new SchedulableTask();
+    }
+
+    /**
+     * Runs or schedules the task to be run.
+     *
+     * @implSpec The recursion which is possible here must be bounded:
+     *
+     *  <pre>{@code
+     *     this.runOrSchedule()
+     *         completer.complete()
+     *             this.runOrSchedule()
+     *                 ...
+     * }</pre>
+     *
+     * @implNote The recursion in this implementation has the maximum
+     * depth of 1.
+     */
+    public void runOrSchedule() {
+        runOrSchedule(schedulableTask, null);
+    }
+
+    /**
+     * Runs or schedules the task to be run in the provided executor.
+     *
+     * <p> This method can be used when potential executing from a calling
+     * thread is not desirable.
+     *
+     * @param executor
+     *         An executor in which to execute the task, if the task needs
+     *         to be executed.
+     *
+     * @apiNote The given executor can be {@code null} in which case calling
+     * {@code deferOrSchedule(null)} is strictly equivalent to calling
+     * {@code runOrSchedule()}.
+     */
+    public void deferOrSchedule(Executor executor) { // TODO: why this name? why not runOrSchedule?
+        runOrSchedule(schedulableTask, executor);
+    }
+
+    private void runOrSchedule(SchedulableTask task, Executor executor) {
+        while (true) {
+            int s = state.get();
+            if (s == END) {
+                if (state.compareAndSet(END, BEGIN)) {
+                    break;
+                }
+            } else if ((s & BEGIN) != 0) {
+                // Tries to change the state to AGAIN, preserving OFFLOAD bit
+                if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
+                    return;
+                }
+            } else if ((s & AGAIN) != 0 || s == STOP) {
+                /* In the case of AGAIN the scheduler does not provide
+                   happens-before relationship between actions prior to
+                   runOrSchedule() and actions that happen in task.run().
+                   The reason is that no volatile write is done in this case,
+                   and the call piggybacks on the call that has actually set
+                   AGAIN state. */
+                return;
+            } else {
+                // Non-existent state, or the one that cannot be offloaded
+                throw new InternalError(String.valueOf(s));
+            }
+        }
+        if (executor == null) {
+            task.run();
+        } else {
+            executor.execute(task);
+        }
+    }
+
+    /** The only concrete {@code DeferredCompleter} implementation. */
+    private class TryEndDeferredCompleter extends DeferredCompleter {
+
+        @Override
+        public void complete() {
+            while (true) {
+                int s;
+                while (((s = state.get()) & OFFLOAD) != 0) {
+                    // Tries to offload ending of the task to the parent
+                    if (state.compareAndSet(s, s & ~OFFLOAD)) {
+                        return;
+                    }
+                }
+                while (true) {
+                    if ((s & OFFLOAD) != 0) {
+                        /* OFFLOAD bit can never be observed here. Otherwise
+                           it would mean there is another invocation of
+                           "complete" that can run the task. */
+                        throw new InternalError(String.valueOf(s));
+                    }
+                    if (s == BEGIN) {
+                        if (state.compareAndSet(BEGIN, END)) {
+                            return;
+                        }
+                    } else if (s == AGAIN) {
+                        if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
+                            break;
+                        }
+                    } else if (s == STOP) {
+                        return;
+                    } else if (s == END) {
+                        throw new IllegalStateException("Duplicate completion");
+                    } else {
+                        // Non-existent state
+                        throw new InternalError(String.valueOf(s));
+                    }
+                    s = state.get();
+                }
+                restartableTask.run(completer);
+            }
+        }
+    }
+
+    /**
+     * Tells whether, or not, this scheduler has been permanently stopped.
+     *
+     * <p> Should be used from inside the task to poll the status of the
+     * scheduler, pretty much the same way as it is done for threads:
+     * <pre>{@code
+     *     if (!Thread.currentThread().isInterrupted()) {
+     *         ...
+     *     }
+     * }</pre>
+     */
+    public boolean isStopped() {
+        return state.get() == STOP;
+    }
+
+    /**
+     * Stops this scheduler.  Subsequent invocations of {@code runOrSchedule}
+     * are effectively no-ops.
+     *
+     * <p> If the task has already begun, this invocation will not affect it,
+     * unless the task itself uses {@code isStopped()} method to check the state
+     * of the handler.
+     */
+    public void stop() {
+        state.set(STOP);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriberWrapper.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.io.Closeable;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A wrapper for a Flow.Subscriber. This wrapper delivers data to the wrapped
+ * Subscriber which is supplied to the constructor. This class takes care of
+ * downstream flow control automatically and upstream flow control automatically
+ * by default.
+ * <p>
+ * Processing is done by implementing the {@link #incoming(List, boolean)} method
+ * which supplies buffers from upstream. This method (or any other method)
+ * can then call the outgoing() method to deliver processed buffers downstream.
+ * <p>
+ * Upstream error signals are delivered downstream directly. Cancellation from
+ * downstream is also propagated upstream immediately.
+ * <p>
+ * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code <Void>}
+ * which propagates completion/errors from downstream to upstream. Normal completion
+ * can only occur after onComplete() is called, but errors can be propagated upwards
+ * at any time.
+ */
+public abstract class SubscriberWrapper
+    implements FlowTube.TubeSubscriber, Closeable, Flow.Processor<List<ByteBuffer>,List<ByteBuffer>>
+                // TODO: SSLTube Subscriber will never change? Does this really need to be a TS?
+{
+    static final boolean DEBUG = Utils.DEBUG;; // Revisit: temporary dev flag.
+    final System.Logger logger =
+            Utils.getDebugLogger(this::dbgString, DEBUG);
+
+    volatile Flow.Subscription upstreamSubscription;
+    final SubscriptionBase downstreamSubscription;
+    volatile boolean upstreamCompleted;
+    volatile boolean downstreamCompleted;
+    volatile boolean completionAcknowledged;
+    private volatile Subscriber<? super List<ByteBuffer>> downstreamSubscriber;
+    // Input Q and lo and hi pri output Qs.
+    private final ConcurrentLinkedQueue<List<ByteBuffer>> inputQ;
+    private final CompletableFuture<Void> cf;
+    private final SequentialScheduler pushScheduler;
+    private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+
+    /**
+     * Wraps the given downstream subscriber. For each call to {@link
+     * #onNext(List<ByteBuffer>) } the given filter function is invoked
+     * and the list (if not empty) returned is passed downstream.
+     *
+     * A {@code CompletableFuture} is supplied which can be used to signal an
+     * error from downstream and which terminates the wrapper or which signals
+     * completion of downstream activity which can be propagated upstream. Error
+     * completion can be signaled at any time, but normal completion must not be
+     * signaled before onComplete() is called.
+     */
+    public SubscriberWrapper()
+    {
+        this.inputQ = new ConcurrentLinkedQueue<>();
+        this.cf = new CompletableFuture<>();
+        this.pushScheduler = new SequentialScheduler(new DownstreamPusher());
+        this.downstreamSubscription = new SubscriptionBase(pushScheduler,
+                                                           this::downstreamCompletion);
+    }
+
+    @Override
+    public final void subscribe(Subscriber<?  super List<ByteBuffer>> downstreamSubscriber) {
+        Objects.requireNonNull(downstreamSubscriber);
+        this.downstreamSubscriber = downstreamSubscriber;
+    }
+
+    /**
+     * Wraps the given downstream wrapper in this. For each call to
+     * {@link #onNext(List<ByteBuffer>) } the incoming() method is called.
+     *
+     * The {@code downstreamCF} from the downstream wrapper is linked to this
+     * wrappers notifier.
+     *
+     * @param downstreamWrapper downstream destination
+     */
+    public SubscriberWrapper(Subscriber<? super List<ByteBuffer>> downstreamWrapper)
+    {
+        this();
+        subscribe(downstreamWrapper);
+    }
+
+    /**
+     * Delivers data to be processed by this wrapper. Generated data to be sent
+     * downstream, must be provided to the {@link #outgoing(List, boolean)}}
+     * method.
+     *
+     * @param buffers a List of ByteBuffers.
+     * @param complete if true then no more data will be added to the list
+     */
+    protected abstract void incoming(List<ByteBuffer> buffers, boolean complete);
+
+    /**
+     * This method is called to determine the window size to use at any time. The
+     * current window is supplied together with the current downstream queue size.
+     * {@code 0} should be returned if no change is
+     * required or a positive integer which will be added to the current window.
+     * The default implementation maintains a downstream queue size of no greater
+     * than 5. The method can be overridden if required.
+     *
+     * @param currentWindow the current upstream subscription window
+     * @param downstreamQsize the current number of buffers waiting to be sent
+     *                        downstream
+     *
+     * @return value to add to currentWindow
+     */
+    protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+        if (downstreamQsize > 5) {
+            return 0;
+        }
+
+        if (currentWindow == 0) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Override this if anything needs to be done after the upstream subscriber
+     * has subscribed
+     */
+    protected void onSubscribe() {
+    }
+
+    /**
+     * Delivers buffers of data downstream. After incoming()
+     * has been called complete == true signifying completion of the upstream
+     * subscription, data may continue to be delivered, up to when outgoing() is
+     * called complete == true, after which, the downstream subscription is
+     * completed.
+     *
+     * It's an error to call outgoing() with complete = true if incoming() has
+     * not previously been called with it.
+     */
+    public void outgoing(ByteBuffer buffer, boolean complete) {
+        Objects.requireNonNull(buffer);
+        assert !complete || !buffer.hasRemaining();
+        outgoing(List.of(buffer), complete);
+    }
+
+    public void outgoing(List<ByteBuffer> buffers, boolean complete) {
+        Objects.requireNonNull(buffers);
+        if (complete) {
+            assert Utils.remaining(buffers) == 0;
+            logger.log(Level.DEBUG, "completionAcknowledged");
+            if (!upstreamCompleted)
+                throw new IllegalStateException("upstream not completed");
+            completionAcknowledged = true;
+        } else {
+            logger.log(Level.DEBUG, () -> "Adding "
+                                   + Utils.remaining(buffers)
+                                   + " to inputQ queue");
+            inputQ.add(buffers);
+        }
+        logger.log(Level.DEBUG, () -> "pushScheduler "
+                   + (pushScheduler.isStopped() ? " is stopped!" : " is alive"));
+        pushScheduler.runOrSchedule();
+    }
+
+    /**
+     * Returns a CompletableFuture which completes when this wrapper completes.
+     * Normal completion happens with the following steps (in order):
+     *   1. onComplete() is called
+     *   2. incoming() called with complete = true
+     *   3. outgoing() may continue to be called normally
+     *   4. outgoing called with complete = true
+     *   5. downstream subscriber is called onComplete()
+     *
+     * If the subscription is canceled or onComplete() is invoked the
+     * CompletableFuture completes exceptionally. Exceptional completion
+     * also occurs if downstreamCF completes exceptionally.
+     */
+    public CompletableFuture<Void> completion() {
+        return cf;
+    }
+
+    /**
+     * Invoked whenever it 'may' be possible to push buffers downstream.
+     */
+    class DownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
+        @Override
+        public void run() {
+            try {
+                run1();
+            } catch (Throwable t) {
+                errorCommon(t);
+            }
+        }
+
+        private void run1() {
+            if (downstreamCompleted) {
+                logger.log(Level.DEBUG, "DownstreamPusher: downstream is already completed");
+                return;
+            }
+
+            // If there was an error, send it downstream.
+            Throwable error = errorRef.get();
+            if (error != null) {
+                synchronized(this) {
+                    if (downstreamCompleted) return;
+                    downstreamCompleted = true;
+                }
+                logger.log(Level.DEBUG,
+                        () -> "DownstreamPusher: forwarding error downstream: " + error);
+                pushScheduler.stop();
+                inputQ.clear();
+                downstreamSubscriber.onError(error);
+                return;
+            }
+
+            // OK - no error, let's proceed
+            if (!inputQ.isEmpty()) {
+                logger.log(Level.DEBUG,
+                    "DownstreamPusher: queue not empty, downstreamSubscription: %s",
+                     downstreamSubscription);
+            } else {
+                logger.log(Level.DEBUG,
+                       "DownstreamPusher: queue empty, downstreamSubscription: %s",
+                       downstreamSubscription);
+            }
+
+            final boolean dbgOn = logger.isLoggable(Level.DEBUG);
+            while (!inputQ.isEmpty() && downstreamSubscription.tryDecrement()) {
+                List<ByteBuffer> b = inputQ.poll();
+                if (dbgOn) logger.log(Level.DEBUG,
+                                            "DownstreamPusher: Pushing "
+                                            + Utils.remaining(b)
+                                            + " bytes downstream");
+                downstreamSubscriber.onNext(b);
+            }
+            upstreamWindowUpdate();
+            checkCompletion();
+        }
+    }
+
+    AtomicLong upstreamWindow = new AtomicLong(0);
+
+    void upstreamWindowUpdate() {
+        long downstreamQueueSize = inputQ.size();
+        long n = upstreamWindowUpdate(upstreamWindow.get(), downstreamQueueSize);
+        if (n > 0)
+            upstreamRequest(n);
+    }
+
+    @Override
+    public void onSubscribe(Flow.Subscription subscription) {
+        if (upstreamSubscription != null) {
+            throw new IllegalStateException("Single shot publisher");
+        }
+        this.upstreamSubscription = subscription;
+        upstreamRequest(upstreamWindowUpdate(0, 0));
+        logger.log(Level.DEBUG,
+               "calling downstreamSubscriber::onSubscribe on %s",
+               downstreamSubscriber);
+        downstreamSubscriber.onSubscribe(downstreamSubscription);
+        onSubscribe();
+    }
+
+    @Override
+    public void onNext(List<ByteBuffer> item) {
+        logger.log(Level.DEBUG, "onNext");
+        long prev = upstreamWindow.getAndDecrement();
+        if (prev <= 0)
+            throw new IllegalStateException("invalid onNext call");
+        incomingCaller(item, false);
+        upstreamWindowUpdate();
+    }
+
+    private void upstreamRequest(long n) {
+        logger.log(Level.DEBUG, "requesting %d", n);
+        upstreamWindow.getAndAdd(n);
+        upstreamSubscription.request(n);
+    }
+
+    public long upstreamWindow() {
+        return upstreamWindow.get();
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+        logger.log(Level.DEBUG, () -> "onError: " + throwable);
+        errorCommon(Objects.requireNonNull(throwable));
+    }
+
+    protected boolean errorCommon(Throwable throwable) {
+        assert throwable != null;
+        if (errorRef.compareAndSet(null, throwable)) {
+            logger.log(Level.DEBUG, "error", throwable);
+            pushScheduler.runOrSchedule();
+            upstreamCompleted = true;
+            cf.completeExceptionally(throwable);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void close() {
+        errorCommon(new RuntimeException("wrapper closed"));
+    }
+
+    private void incomingCaller(List<ByteBuffer> l, boolean complete) {
+        try {
+            incoming(l, complete);
+        } catch(Throwable t) {
+            errorCommon(t);
+        }
+    }
+
+    @Override
+    public void onComplete() {
+        logger.log(Level.DEBUG, () -> "upstream completed: " + toString());
+        upstreamCompleted = true;
+        incomingCaller(Utils.EMPTY_BB_LIST, true);
+        checkCompletion();
+        pushScheduler.runOrSchedule();
+    }
+
+    /** Adds the given data to the input queue. */
+    public void addData(ByteBuffer l) {
+        if (upstreamSubscription == null) {
+            throw new IllegalStateException("can't add data before upstream subscriber subscribes");
+        }
+        incomingCaller(List.of(l), false);
+    }
+
+    void checkCompletion() {
+        if (downstreamCompleted || !upstreamCompleted) {
+            return;
+        }
+        if (!inputQ.isEmpty()) {
+            return;
+        }
+        if (errorRef.get() != null) {
+            pushScheduler.runOrSchedule();
+            return;
+        }
+        if (completionAcknowledged) {
+            downstreamSubscriber.onComplete();
+            // Fix me subscriber.onComplete.run();
+            downstreamCompleted = true;
+            cf.complete(null);
+        }
+    }
+
+    // called from the downstream Subscription.cancel()
+    void downstreamCompletion() {
+        upstreamSubscription.cancel();
+        cf.complete(null);
+    }
+
+    public void resetDownstreamDemand() {
+        downstreamSubscription.demand.reset();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("SubscriberWrapper:")
+          .append(" upstreamCompleted: ").append(Boolean.toString(upstreamCompleted))
+          .append(" upstreamWindow: ").append(upstreamWindow.toString())
+          .append(" downstreamCompleted: ").append(Boolean.toString(downstreamCompleted))
+          .append(" completionAcknowledged: ").append(Boolean.toString(completionAcknowledged))
+          .append(" inputQ size: ").append(Integer.toString(inputQ.size()))
+          //.append(" inputQ: ").append(inputQ.toString())
+          .append(" cf: ").append(cf.toString())
+          .append(" downstreamSubscription: ").append(downstreamSubscription.toString());
+
+        return sb.toString();
+    }
+
+    public String dbgString() {
+        return "SubscriberWrapper";
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SubscriptionBase.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Maintains subscription counter and provides primitives for
+ * - accessing window
+ * - reducing window when delivering items externally
+ * - resume delivery when window was zero previously
+ *
+ * @author mimcmah
+ */
+public class SubscriptionBase implements Flow.Subscription {
+
+    final Demand demand = new Demand();
+
+    final SequentialScheduler scheduler; // when window was zero and is opened, run this
+    final Runnable cancelAction; // when subscription cancelled, run this
+    final AtomicBoolean cancelled;
+
+    public SubscriptionBase(SequentialScheduler scheduler, Runnable cancelAction) {
+        this.scheduler = scheduler;
+        this.cancelAction = cancelAction;
+        this.cancelled = new AtomicBoolean(false);
+    }
+
+    @Override
+    public void request(long n) {
+        if (demand.increase(n))
+            scheduler.runOrSchedule();
+    }
+
+
+
+    @Override
+    public synchronized String toString() {
+        return "SubscriptionBase: window = " + demand.get() +
+                " cancelled = " + cancelled.toString();
+    }
+
+    /**
+     * Returns true if the window was reduced by 1. In that case
+     * items must be supplied to subscribers and the scheduler run
+     * externally. If the window could not be reduced by 1, then false
+     * is returned and the scheduler will run later when the window is updated.
+     */
+    public boolean tryDecrement() {
+        return demand.tryDecrement();
+    }
+
+    public long window() {
+        return demand.get();
+    }
+
+    @Override
+    public void cancel() {
+        if (cancelled.getAndSet(true))
+            return;
+        scheduler.stop();
+        cancelAction.run();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/SynchronousPublisher.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.WeakHashMap;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * This publisher signals {@code onNext} synchronously and
+ * {@code onComplete}/{@code onError} asynchronously to its only subscriber.
+ *
+ * <p> This publisher supports a single subscriber over this publisher's
+ * lifetime. {@code signalComplete} and {@code signalError} may be called before
+ * the subscriber has subscribed.
+ *
+ * <p> The subscriber's requests are signalled to the subscription supplied to
+ * the {@code feedback} method.
+ *
+ * <p> {@code subscribe} and {@code feedback} methods can be called in any
+ * order.
+ *
+ * <p> {@code signalNext} may be called recursively, the implementation will
+ * bound the depth of the recursion.
+ *
+ * <p> It is always an error to call {@code signalNext} without a sufficient
+ * demand.
+ *
+ * <p> If subscriber throws an exception from any of its methods, the
+ * subscription will be cancelled.
+ */
+public final class SynchronousPublisher<T> implements Publisher<T> {
+    /*
+     * PENDING, ACTIVE and CANCELLED are states. TERMINATE and DELIVERING are
+     * state modifiers, they cannot appear in the state on their own.
+     *
+     * PENDING, ACTIVE and CANCELLED are mutually exclusive states. Any two of
+     * those bits cannot be set at the same time in state.
+     *
+     * PENDING -----------------> ACTIVE <------> DELIVERING
+     *    |                         |
+     *    +------> TERMINATE <------+
+     *    |            |            |
+     *    |            v            |
+     *    +------> CANCELLED <------+
+     *
+     * The following states are allowed:
+     *
+     *     PENDING
+     *     PENDING | TERMINATE,
+     *     ACTIVE,
+     *     ACTIVE | DELIVERING,
+     *     ACTIVE | TERMINATE,
+     *     ACTIVE | DELIVERING | TERMINATE
+     *     CANCELLED
+     */
+    /**
+     * A state modifier meaning {@code onSubscribe} has not been called yet.
+     *
+     * <p> After {@code onSubscribe} has been called the machine can transition
+     * into {@code ACTIVE}, {@code PENDING | TERMINATE} or {@code CANCELLED}.
+     */
+    private static final int PENDING = 1;
+    /**
+     * A state modifier meaning {@code onSubscribe} has been called, no error
+     * and no completion has been signalled and {@code onNext} may be called.
+     */
+    private static final int ACTIVE = 2;
+    /**
+     * A state modifier meaning no calls to subscriber may be made.
+     *
+     * <p> Once this modifier is set, it will not be unset. It's a final state.
+     */
+    private static final int CANCELLED = 4;
+    /**
+     * A state modifier meaning {@code onNext} is being called and no other
+     * signal may be made.
+     *
+     * <p> This bit can be set at any time. signalNext uses it to ensure the
+     * method is called sequentially.
+     */
+    private static final int DELIVERING = 8;
+    /**
+     * A state modifier meaning the next call must be either {@code onComplete}
+     * or {@code onError}.
+     *
+     * <p> The concrete method depends on the value of {@code terminationType}).
+     * {@code TERMINATE} bit cannot appear on its own, it can be set only with
+     * {@code PENDING} or {@code ACTIVE}.
+     */
+    private static final int TERMINATE = 16;
+    /**
+     * Current demand. If fulfilled, no {@code onNext} signals may be made.
+     */
+    private final Demand demand = new Demand();
+    /**
+     * The current state of the subscription. Contains disjunctions of the above
+     * state modifiers.
+     */
+    private final AtomicInteger state = new AtomicInteger(PENDING);
+    /**
+     * A convenient way to represent 3 values: not set, completion and error.
+     */
+    private final AtomicReference<Optional<Throwable>> terminationType
+            = new AtomicReference<>();
+    /**
+     * {@code signalNext} uses this lock to ensure the method is called in a
+     * thread-safe manner.
+     */
+    private final ReentrantLock nextLock = new ReentrantLock();
+    private T next;
+
+    private final Object lock = new Object();
+    /**
+     * This map stores the subscribers attempted to subscribe to this publisher.
+     * It is needed so this publisher does not call {@code onSubscribe} on a
+     * subscriber more than once (Rule 2.12).
+     *
+     * <p> It will most likely have a single entry for the only subscriber.
+     * Because this publisher is one-off, subscribing to it more than once is an
+     * error.
+     */
+    private final Map<Subscriber<?>, Object> knownSubscribers
+            = new WeakHashMap<>(1, 1);
+    /**
+     * The active subscriber. This reference will be reset to {@code null} once
+     * the subscription becomes cancelled (Rule 3.13).
+     */
+    private volatile Subscriber<? super T> subscriber;
+    /**
+     * A temporary subscription that receives all calls to
+     * {@code request}/{@code cancel} until two things happen: (1) the feedback
+     * becomes set and (2) {@code onSubscribe} method is called on the
+     * subscriber.
+     *
+     * <p> The first condition is obvious. The second one is about not
+     * propagating requests to {@code feedback} until {@code onSubscribe} call
+     * has been finished. The reason is that Rule 1.3 requires the subscriber
+     * methods to be called in a thread-safe manner. This, in particular,
+     * implies that if called from multiple threads, the calls must not be
+     * concurrent. If, for instance, {@code subscription.request(long)) (and
+     * this is a usual state of affairs) is called from within
+     * {@code onSubscribe} call, the publisher will have to resort to some sort
+     * of queueing (locks, queues, etc.) of possibly arriving {@code onNext}
+     * signals while in {@code onSubscribe}. This publisher doesn't queue
+     * signals, instead it "queues" requests. Because requests are just numbers
+     * and requests are additive, the effective queue is a single number of
+     * total requests made so far.
+     */
+    private final TemporarySubscription temporarySubscription
+            = new TemporarySubscription();
+    private volatile Subscription feedback;
+    /**
+     * Keeping track of whether a subscription may be made. (The {@code
+     * subscriber} field may later become {@code null}, but this flag is
+     * permanent. Once {@code true} forever {@code true}.
+     */
+    private boolean subscribed;
+
+    @Override
+    public void subscribe(Subscriber<? super T> sub) {
+        Objects.requireNonNull(sub);
+        boolean success = false;
+        boolean duplicate = false;
+        synchronized (lock) {
+            if (!subscribed) {
+                subscribed = true;
+                subscriber = sub;
+                assert !knownSubscribers.containsKey(subscriber);
+                knownSubscribers.put(subscriber, null);
+                success = true;
+            } else if (sub.equals(subscriber)) {
+                duplicate = true;
+            } else if (!knownSubscribers.containsKey(sub)) {
+                knownSubscribers.put(sub, null);
+            } else {
+                return;
+            }
+        }
+        if (success) {
+            signalSubscribe();
+        } else if (duplicate) {
+            signalError(new IllegalStateException("Duplicate subscribe"));
+        } else {
+            // This is a best-effort attempt for an isolated publisher to call
+            // a foreign subscriber's methods in a sequential order. However it
+            // cannot be guaranteed unless all publishers share information on
+            // all subscribers in the system. This publisher does its job right.
+            sub.onSubscribe(new NopSubscription());
+            sub.onError(new IllegalStateException("Already subscribed"));
+        }
+    }
+
+    /**
+     * Accepts a subscription that is signalled with the subscriber's requests.
+     *
+     * @throws NullPointerException
+     *         if {@code subscription} is {@code null}
+     * @throws IllegalStateException
+     *         if there is a feedback subscription already
+     */
+    public void feedback(Subscription subscription) {
+        Objects.requireNonNull(subscription);
+        synchronized (lock) {
+            if (feedback != null) {
+                throw new IllegalStateException(
+                        "Already has a feedback subscription");
+            }
+            feedback = subscription;
+            if ((state.get() & PENDING) == 0) {
+                temporarySubscription.replaceWith(new PermanentSubscription());
+            }
+        }
+    }
+
+    /**
+     * Tries to deliver the specified item to the subscriber.
+     *
+     * <p> The item may not be delivered even if there is a demand. This can
+     * happen as a result of subscriber cancelling the subscription by
+     * signalling {@code cancel} or this publisher cancelling the subscription
+     * by signaling {@code onError} or {@code onComplete}.
+     *
+     * <p> Given no exception is thrown, a call to this method decremented the
+     * demand.
+     *
+     * @param item
+     *         the item to deliver to the subscriber
+     *
+     * @return {@code true} iff the subscriber has received {@code item}
+     * @throws NullPointerException
+     *         if {@code item} is {@code null}
+     * @throws IllegalStateException
+     *         if there is no demand
+     * @throws IllegalStateException
+     *         the method is called concurrently
+     */
+    public boolean signalNext(T item) {
+        Objects.requireNonNull(item);
+        if (!nextLock.tryLock()) {
+            throw new IllegalStateException("Concurrent signalling");
+        }
+        boolean recursion = false;
+        try {
+            next = item;
+            while (true) {
+                int s = state.get();
+                if ((s & DELIVERING) == DELIVERING) {
+                    recursion = true;
+                    break;
+                } else if (state.compareAndSet(s, s | DELIVERING)) {
+                    break;
+                }
+            }
+            if (!demand.tryDecrement()) {
+                // Hopefully this will help to find bugs in this publisher's
+                // clients. Because signalNext should never be issues without
+                // having a sufficient demand. Even if the thing is cancelled!
+//                next = null;
+                throw new IllegalStateException("No demand");
+            }
+            if (recursion) {
+                return true;
+            }
+            while (next != null) {
+                int s = state.get();
+                if ((s & (ACTIVE | TERMINATE)) == (ACTIVE | TERMINATE)) {
+                    if (state.compareAndSet(
+                            s, CANCELLED | (s & ~(ACTIVE | TERMINATE)))) {
+                        // terminationType must be read only after the
+                        // termination condition has been observed
+                        // (those have been stored in the opposite order)
+                        Optional<Throwable> t = terminationType.get();
+                        dispatchTerminationAndUnsubscribe(t);
+                        return false;
+                    }
+                } else if ((s & ACTIVE) == ACTIVE) {
+                    try {
+                        T t = next;
+                        next = null;
+                        subscriber.onNext(t);
+                    } catch (Throwable t) {
+                        cancelNow();
+                        throw t;
+                    }
+                } else if ((s & CANCELLED) == CANCELLED) {
+                    return false;
+                } else if ((s & PENDING) == PENDING) {
+                    // Actually someone called signalNext even before
+                    // onSubscribe has been called, but from this publisher's
+                    // API point of view it's still "No demand"
+                    throw new IllegalStateException("No demand");
+                } else {
+                    throw new InternalError(String.valueOf(s));
+                }
+            }
+            return true;
+        } finally {
+            while (!recursion) { // If the call was not recursive unset the bit
+                int s = state.get();
+                if ((s & DELIVERING) != DELIVERING) {
+                    throw new InternalError(String.valueOf(s));
+                } else if (state.compareAndSet(s, s & ~DELIVERING)) {
+                    break;
+                }
+            }
+            nextLock.unlock();
+        }
+    }
+
+    /**
+     * Cancels the subscription by signalling {@code onError} to the subscriber.
+     *
+     * <p> Will not signal {@code onError} if the subscription has been
+     * cancelled already.
+     *
+     * <p> This method may be called at any time.
+     *
+     * @param error
+     *         the error to signal
+     *
+     * @throws NullPointerException
+     *         if {@code error} is {@code null}
+     */
+    public void signalError(Throwable error) {
+        terminateNow(Optional.of(error));
+    }
+
+    /**
+     * Cancels the subscription by signalling {@code onComplete} to the
+     * subscriber.
+     *
+     * <p> Will not signal {@code onComplete} if the subscription has been
+     * cancelled already.
+     *
+     * <p> This method may be called at any time.
+     */
+    public void signalComplete() {
+        terminateNow(Optional.empty());
+    }
+
+    /**
+     * Must be called first and at most once.
+     */
+    private void signalSubscribe() {
+        assert subscribed;
+        try {
+            subscriber.onSubscribe(temporarySubscription);
+        } catch (Throwable t) {
+            cancelNow();
+            throw t;
+        }
+        while (true) {
+            int s = state.get();
+            if ((s & (PENDING | TERMINATE)) == (PENDING | TERMINATE)) {
+                if (state.compareAndSet(
+                        s, CANCELLED | (s & ~(PENDING | TERMINATE)))) {
+                    Optional<Throwable> t = terminationType.get();
+                    dispatchTerminationAndUnsubscribe(t);
+                    return;
+                }
+            } else if ((s & PENDING) == PENDING) {
+                if (state.compareAndSet(s, ACTIVE | (s & ~PENDING))) {
+                    synchronized (lock) {
+                        if (feedback != null) {
+                            temporarySubscription
+                                    .replaceWith(new PermanentSubscription());
+                        }
+                    }
+                    return;
+                }
+            } else { // It should not be in any other state
+                throw new InternalError(String.valueOf(s));
+            }
+        }
+    }
+
+    private void unsubscribe() {
+        subscriber = null;
+    }
+
+    private final static class NopSubscription implements Subscription {
+
+        @Override
+        public void request(long n) { }
+        @Override
+        public void cancel() { }
+    }
+
+    private final class PermanentSubscription implements Subscription {
+
+        @Override
+        public void request(long n) {
+            if (n <= 0) {
+                signalError(new IllegalArgumentException(
+                        "non-positive subscription request"));
+            } else {
+                demand.increase(n);
+                feedback.request(n);
+            }
+        }
+
+        @Override
+        public void cancel() {
+            if (cancelNow()) {
+                unsubscribe();
+                // feedback.cancel() is called at most once
+                // (let's not assume idempotency)
+                feedback.cancel();
+            }
+        }
+    }
+
+    /**
+     * Cancels the subscription unless it has been cancelled already.
+     *
+     * @return {@code true} iff the subscription has been cancelled as a result
+     *         of this call
+     */
+    private boolean cancelNow() {
+        while (true) {
+            int s = state.get();
+            if ((s & CANCELLED) == CANCELLED) {
+                return false;
+            } else if ((s & (ACTIVE | PENDING)) != 0) {
+                // ACTIVE or PENDING
+                if (state.compareAndSet(
+                        s, CANCELLED | (s & ~(ACTIVE | PENDING)))) {
+                    unsubscribe();
+                    return true;
+                }
+            } else {
+                throw new InternalError(String.valueOf(s));
+            }
+        }
+    }
+
+    /**
+     * Terminates this subscription unless is has been cancelled already.
+     *
+     * @param t the type of termination
+     */
+    private void terminateNow(Optional<Throwable> t) {
+        // Termination condition must be set only after the termination
+        // type has been set (those will be read in the opposite order)
+        if (!terminationType.compareAndSet(null, t)) {
+            return;
+        }
+        while (true) {
+            int s = state.get();
+            if ((s & CANCELLED) == CANCELLED) {
+                return;
+            } else if ((s & (PENDING | DELIVERING)) != 0) {
+                // PENDING or DELIVERING (which implies ACTIVE)
+                if (state.compareAndSet(s, s | TERMINATE)) {
+                    return;
+                }
+            } else if ((s & ACTIVE) == ACTIVE) {
+                if (state.compareAndSet(s, CANCELLED | (s & ~ACTIVE))) {
+                    dispatchTerminationAndUnsubscribe(t);
+                    return;
+                }
+            } else {
+                throw new InternalError(String.valueOf(s));
+            }
+        }
+    }
+
+    private void dispatchTerminationAndUnsubscribe(Optional<Throwable> t) {
+        try {
+            t.ifPresentOrElse(subscriber::onError, subscriber::onComplete);
+        } finally {
+            unsubscribe();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/TemporarySubscription.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.common;
+
+import jdk.incubator.http.internal.common.SequentialScheduler.CompleteRestartableTask;
+
+import java.util.Objects;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Acts as a subscription receiving calls to {@code request} and {@code cancel}
+ * methods until the replacing subscription is set.
+ *
+ * <p> After the replacing subscription is set, it gets updated with the result
+ * of calls happened before that and starts receiving calls to its
+ * {@code request} and {@code cancel} methods.
+ *
+ * <p> This subscription ensures that {@code request} and {@code cancel} methods
+ * of the replacing subscription are called sequentially.
+ */
+public final class TemporarySubscription implements Subscription {
+
+    private final AtomicReference<Subscription> subscription = new AtomicReference<>();
+    private final Demand demand = new Demand();
+    private volatile boolean cancelled;
+    private volatile long illegalValue = 1;
+
+    private final SequentialScheduler scheduler = new SequentialScheduler(new UpdateTask());
+
+    @Override
+    public void request(long n) {
+        if (n <= 0) {
+            // Any non-positive request would do, no need to remember them
+            // all or any one in particular.
+            // tl;dr racy, but don't care
+            illegalValue = n;
+        } else {
+            demand.increase(n);
+        }
+        scheduler.runOrSchedule();
+    }
+
+    @Override
+    public void cancel() {
+        cancelled = true;
+        scheduler.runOrSchedule();
+    }
+
+    public void replaceWith(Subscription permanentSubscription) {
+        Objects.requireNonNull(permanentSubscription);
+        if (permanentSubscription == this) {
+            // Otherwise it would be an unpleasant bug to chase
+            throw new IllegalStateException("Self replacement");
+        }
+        if (!subscription.compareAndSet(null, permanentSubscription)) {
+            throw new IllegalStateException("Already replaced");
+        }
+        scheduler.runOrSchedule();
+    }
+
+    private final class UpdateTask extends CompleteRestartableTask {
+
+        @Override
+        public void run() {
+            Subscription dst = TemporarySubscription.this.subscription.get();
+            if (dst == null) {
+                return;
+            }
+            /* As long as the result is effectively the same, it does not matter
+               how requests are accumulated and what goes first: request or
+               cancel. See rules 3.5, 3.6, 3.7 and 3.9 from the reactive-streams
+               specification. */
+            long illegalValue = TemporarySubscription.this.illegalValue;
+            if (illegalValue <= 0) {
+                dst.request(illegalValue);
+                scheduler.stop();
+            } else if (cancelled) {
+                dst.cancel();
+                scheduler.stop();
+            } else {
+                long accumulatedValue = demand.decreaseAndGet(Long.MAX_VALUE);
+                if (accumulatedValue > 0) {
+                    dst.request(accumulatedValue);
+                }
+            }
+        }
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Sun Nov 05 17:32:13 2017 +0000
@@ -36,6 +36,8 @@
 import java.io.UncheckedIOException;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.NetPermission;
 import java.net.URI;
@@ -47,17 +49,22 @@
 import java.security.PrivilegedAction;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.LinkedList;
 import java.util.Map;
 import java.util.Optional;
+import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import jdk.incubator.http.HttpHeaders;
 
 /**
@@ -65,6 +72,17 @@
  */
 public final class Utils {
 
+    public static final boolean ASSERTIONSENABLED;
+    static {
+        boolean enabled = false;
+        assert enabled = true;
+        ASSERTIONSENABLED = enabled;
+    }
+    public static final boolean DEBUG = true;// Revisit: temporary dev flag.
+            //getBooleanProperty(DebugLogger.HTTP_NAME, false);
+    public static final boolean DEBUG_HPACK = // Revisit: temporary dev flag.
+            getBooleanProperty(DebugLogger.HPACK_NAME, false);
+
     /**
      * Allocated buffer size. Must never be higher than 16K. But can be lower
      * if smaller allocation units preferred. HTTP/2 mandates that all
@@ -91,6 +109,19 @@
         return ByteBuffer.allocate(BUFSIZE);
     }
 
+    // Used when we know the max amount we want to put in the buffer
+    // In that case there's no reason to allocate a greater amount.
+    // Still not allow to allocate more than BUFSIZE.
+    public static ByteBuffer getBufferWithAtMost(int maxAmount) {
+        return ByteBuffer.allocate(Math.min(BUFSIZE, maxAmount));
+    }
+
+    public static Throwable getCompletionCause(Throwable x) {
+        if (!(x instanceof CompletionException)) return x;
+        final Throwable cause = x.getCause();
+        return cause == null ? x : cause;
+    }
+
     public static IOException getIOException(Throwable t) {
         if (t instanceof IOException) {
             return (IOException) t;
@@ -103,16 +134,6 @@
     }
 
     /**
-     * We use the same buffer for reading all headers and dummy bodies in an Exchange.
-     */
-    public static ByteBuffer getExchangeBuffer() {
-        ByteBuffer buf = getBuffer();
-        // Force a read the first time it is used
-        buf.limit(0);
-        return buf;
-    }
-
-    /**
      * Puts position to limit and limit to capacity so we can resume reading
      * into this buffer, but if required > 0 then limit may be reduced so that
      * no more than required bytes are read next time.
@@ -130,11 +151,6 @@
 
     private Utils() { }
 
-    public static ExecutorService innocuousThreadPool() {
-        return Executors.newCachedThreadPool(
-                (r) -> InnocuousThread.newThread("DefaultHttpClient", r));
-    }
-
     // ABNF primitives defined in RFC 7230
     private static final boolean[] tchar      = new boolean[256];
     private static final boolean[] fieldvchar = new boolean[256];
@@ -217,46 +233,6 @@
         return accepted;
     }
 
-    /**
-     * Returns the security permission required for the given details.
-     * If method is CONNECT, then uri must be of form "scheme://host:port"
-     */
-    public static URLPermission getPermission(URI uri,
-                                              String method,
-                                              Map<String, List<String>> headers) {
-        StringBuilder sb = new StringBuilder();
-
-        String urlstring, actionstring;
-
-        if (method.equals("CONNECT")) {
-            urlstring = uri.toString();
-            actionstring = "CONNECT";
-        } else {
-            sb.append(uri.getScheme())
-                    .append("://")
-                    .append(uri.getAuthority())
-                    .append(uri.getPath());
-            urlstring = sb.toString();
-
-            sb = new StringBuilder();
-            sb.append(method);
-            if (headers != null && !headers.isEmpty()) {
-                sb.append(':');
-                Set<String> keys = headers.keySet();
-                boolean first = true;
-                for (String key : keys) {
-                    if (!first) {
-                        sb.append(',');
-                    }
-                    sb.append(key);
-                    first = false;
-                }
-            }
-            actionstring = sb.toString();
-        }
-        return new URLPermission(urlstring, actionstring);
-    }
-
     public static void checkNetPermission(String target) {
         SecurityManager sm = System.getSecurityManager();
         if (sm == null) {
@@ -266,6 +242,14 @@
         sm.checkPermission(np);
     }
 
+    public static void sleep(int millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
     public static int getIntegerNetProperty(String name, int defaultValue) {
         return AccessController.doPrivileged((PrivilegedAction<Integer>) () ->
                 NetProperties.getInteger(name, defaultValue));
@@ -276,6 +260,11 @@
                 NetProperties.get(name));
     }
 
+    static boolean getBooleanProperty(String name, boolean def) {
+        return AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
+                Boolean.parseBoolean(System.getProperty(name, String.valueOf(def))));
+    }
+
     public static SSLParameters copySSLParameters(SSLParameters p) {
         SSLParameters p1 = new SSLParameters();
         p1.setAlgorithmConstraints(p.getAlgorithmConstraints());
@@ -337,6 +326,49 @@
         return srcLen - src.remaining();
     }
 
+    /** Threshold beyond which data is no longer copied into the current
+     * buffer, if that buffer has enough unused space. */
+    private static final int COPY_THRESHOLD = 8192;
+
+    /**
+     * Adds the data from buffersToAdd to currentList. Either 1) appends the
+     * data from a particular buffer to the last buffer in the list ( if
+     * there is enough unused space ), or 2) adds it to the list.
+     *
+     * @returns the number of bytes added
+     */
+    public static long accumulateBuffers(List<ByteBuffer> currentList,
+                                         List<ByteBuffer> buffersToAdd) {
+        long accumulatedBytes = 0;
+        for (ByteBuffer bufferToAdd : buffersToAdd) {
+            int remaining = bufferToAdd.remaining();
+            if (remaining <= 0)
+                continue;
+            int listSize = currentList.size();
+            if (listSize == 0) {
+                currentList.add(bufferToAdd);
+                accumulatedBytes = remaining;
+                continue;
+            }
+
+            ByteBuffer lastBuffer = currentList.get(currentList.size() - 1);
+            int freeSpace = lastBuffer.capacity() - lastBuffer.limit();
+            if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
+                // append the new data to the unused space in the last buffer
+                int position = lastBuffer.position();
+                int limit = lastBuffer.limit();
+                lastBuffer.position(limit);
+                lastBuffer.limit(limit + bufferToAdd.limit());
+                lastBuffer.put(bufferToAdd);
+                lastBuffer.position(position);
+            } else {
+                currentList.add(bufferToAdd);
+            }
+            accumulatedBytes += remaining;
+        }
+        return accumulatedBytes;
+    }
+
     // copy up to amount from src to dst, but no more
     public static int copyUpTo(ByteBuffer src, ByteBuffer dst, int amount) {
         int toCopy = Math.min(src.remaining(), Math.min(dst.remaining(), amount));
@@ -378,30 +410,81 @@
         return Arrays.toString(source.toArray());
     }
 
-    public static int remaining(ByteBuffer[] bufs) {
-        int remain = 0;
+    public static int remaining(ByteBuffer buf) {
+        return buf.remaining();
+    }
+
+    public static long remaining(ByteBuffer[] bufs) {
+        long remain = 0;
         for (ByteBuffer buf : bufs) {
             remain += buf.remaining();
         }
         return remain;
     }
 
-    public static int remaining(List<ByteBuffer> bufs) {
-        int remain = 0;
-        for (ByteBuffer buf : bufs) {
-            remain += buf.remaining();
+    public static boolean hasRemaining(List<ByteBuffer> bufs) {
+        synchronized (bufs) {
+            for (ByteBuffer buf : bufs) {
+                if (buf.hasRemaining())
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    public static long remaining(List<ByteBuffer> bufs) {
+        long remain = 0;
+        synchronized (bufs) {
+            for (ByteBuffer buf : bufs) {
+                remain += buf.remaining();
+            }
         }
         return remain;
     }
 
-    public static int remaining(ByteBufferReference[] refs) {
-        int remain = 0;
+    public static int remaining(List<ByteBuffer> bufs, int max) {
+        long remain = 0;
+        synchronized (bufs) {
+            for (ByteBuffer buf : bufs) {
+                remain += buf.remaining();
+                if (remain > max) {
+                    throw new IllegalArgumentException("too many bytes");
+                }
+            }
+        }
+        return (int) remain;
+    }
+
+    public static long remaining(ByteBufferReference[] refs) {
+        long remain = 0;
         for (ByteBufferReference ref : refs) {
             remain += ref.get().remaining();
         }
         return remain;
     }
 
+    public static int remaining(ByteBufferReference[] refs, int max) {
+        long remain = 0;
+        for (ByteBufferReference ref : refs) {
+            remain += ref.get().remaining();
+            if (remain > max) {
+                throw new IllegalArgumentException("too many bytes");
+            }
+        }
+        return (int) remain;
+    }
+
+    public static int remaining(ByteBuffer[] refs, int max) {
+        long remain = 0;
+        for (ByteBuffer b : refs) {
+            remain += b.remaining();
+            if (remain > max) {
+                throw new IllegalArgumentException("too many bytes");
+            }
+        }
+        return (int) remain;
+    }
+
     // assumes buffer was written into starting at position zero
     static void unflip(ByteBuffer buf) {
         buf.position(buf.limit());
@@ -446,50 +529,17 @@
         return new String(b, StandardCharsets.US_ASCII);
     }
 
-    /**
-     * Returns a single threaded executor which uses one invocation
-     * of the parent executor to execute tasks (in sequence).
-     *
-     * Use a null valued Runnable to terminate.
-     */
-    // TODO: this is a blocking way of doing this;
-    public static Executor singleThreadExecutor(Executor parent) {
-        BlockingQueue<Optional<Runnable>> queue = new LinkedBlockingQueue<>();
-        parent.execute(() -> {
-            while (true) {
-                try {
-                    Optional<Runnable> o = queue.take();
-                    if (!o.isPresent()) {
-                        return;
-                    }
-                    o.get().run();
-                } catch (InterruptedException ex) {
-                    return;
-                }
-            }
-        });
-        return new Executor() {
-            @Override
-            public void execute(Runnable command) {
-                queue.offer(Optional.ofNullable(command));
-            }
-        };
-    }
-
-    private static void executeInline(Runnable r) {
-        r.run();
-    }
-
-    static Executor callingThreadExecutor() {
-        return Utils::executeInline;
-    }
-
     // Put all these static 'empty' singletons here
     @SuppressWarnings("rawtypes")
     public static final CompletableFuture[] EMPTY_CFARRAY = new CompletableFuture[0];
 
     public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
     public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0];
+    public static final List<ByteBuffer> EMPTY_BB_LIST;
+
+    static {
+        EMPTY_BB_LIST = Collections.unmodifiableList(new LinkedList<>());
+    }
 
     public static ByteBuffer slice(ByteBuffer buffer, int amount) {
         ByteBuffer newb = buffer.slice();
@@ -515,4 +565,348 @@
     public static UncheckedIOException unchecked(IOException e) {
         return new UncheckedIOException(e);
     }
+
+    /**
+     * Get a logger for debug HTTP traces.
+     *
+     * The logger should only be used with levels whose severity is
+     * {@code <= DEBUG}. By default, this logger will forward all messages
+     * logged to an internal logger named "jdk.internal.httpclient.debug".
+     * In addition, if the property -Djdk.internal.httpclient.debug=true is set,
+     * it will print the messages on stderr.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+     *
+     * @return A logger for HTTP internal debug traces
+     */
+    public static Logger getDebugLogger(Supplier<String> dbgTag) {
+        return getDebugLogger(dbgTag, DEBUG);
+    }
+
+    /**
+     * Get a logger for debug HTTP traces.The logger should only be used
+     * with levels whose severity is {@code <= DEBUG}.
+     *
+     * By default, this logger will forward all messages logged to an internal
+     * logger named "jdk.internal.httpclient.debug".
+     * In addition, if the message severity level is >= to
+     * the provided {@code errLevel} it will print the messages on stderr.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @apiNote To obtain a logger that will always print things on stderr in
+     *          addition to forwarding to the internal logger, use
+     *          {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+     *          This is also equivalent to calling
+     *          {@code getDebugLogger(this::dbgTag, true);}.
+     *          To obtain a logger that will only forward to the internal logger,
+     *          use {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+     *          This is also equivalent to calling
+     *          {@code getDebugLogger(this::dbgTag, false);}.
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+     * @param errLevel The level above which messages will be also printed on
+     *               stderr (in addition to be forwarded to the internal logger).
+     *
+     * @return A logger for HTTP internal debug traces
+     */
+    static Logger getDebugLogger(Supplier<String> dbgTag, Level errLevel) {
+        return new DebugLogger(DebugLogger.HTTP, dbgTag, Level.OFF, errLevel);
+    }
+
+    /**
+     * Get a logger for debug HTTP traces.The logger should only be used
+     * with levels whose severity is {@code <= DEBUG}.
+     *
+     * By default, this logger will forward all messages logged to an internal
+     * logger named "jdk.internal.httpclient.debug".
+     * In addition, the provided boolean {@code on==true}, it will print the
+     * messages on stderr.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @apiNote To obtain a logger that will always print things on stderr in
+     *          addition to forwarding to the internal logger, use
+     *          {@code getDebugLogger(this::dbgTag, true);}.
+     *          This is also equivalent to calling
+     *          {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+     *          To obtain a logger that will only forward to the internal logger,
+     *          use {@code getDebugLogger(this::dbgTag, false);}.
+     *          This is also equivalent to calling
+     *          {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+     * @param on  Whether messages should also be printed on
+     *               stderr (in addition to be forwarded to the internal logger).
+     *
+     * @return A logger for HTTP internal debug traces
+     */
+    public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
+        Level errLevel = on ? Level.ALL : Level.OFF;
+        return getDebugLogger(dbgTag, errLevel);
+    }
+
+    /**
+     * Get a logger for debug HPACK traces.
+     *
+     * The logger should only be used with levels whose severity is
+     * {@code <= DEBUG}. By default, this logger will forward all messages
+     * logged to an internal logger named "jdk.internal.httpclient.hpack.debug".
+     * In addition, if the property -Djdk.internal.httpclient.hpack.debug=true
+     * is set,  it will print the messages on stdout.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+     *
+     * @return A logger for HPACK internal debug traces
+     */
+    public static Logger getHpackLogger(Supplier<String> dbgTag) {
+        Level errLevel = Level.OFF;
+        Level outLevel = DEBUG_HPACK ? Level.ALL : Level.OFF;
+        return new DebugLogger(DebugLogger.HPACK, dbgTag, outLevel, errLevel);
+    }
+
+    /**
+     * Get a logger for debug HPACK traces.The logger should only be used
+     * with levels whose severity is {@code <= DEBUG}.
+     *
+     * By default, this logger will forward all messages logged to an internal
+     * logger named "jdk.internal.httpclient.hpack.debug".
+     * In addition, if the message severity level is >= to
+     * the provided {@code outLevel} it will print the messages on stdout.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @apiNote To obtain a logger that will always print things on stdout in
+     *          addition to forwarding to the internal logger, use
+     *          {@code getHpackLogger(this::dbgTag, Level.ALL);}.
+     *          This is also equivalent to calling
+     *          {@code getHpackLogger(this::dbgTag, true);}.
+     *          To obtain a logger that will only forward to the internal logger,
+     *          use {@code getHpackLogger(this::dbgTag, Level.OFF);}.
+     *          This is also equivalent to calling
+     *          {@code getHpackLogger(this::dbgTag, false);}.
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+     * @param outLevel The level above which messages will be also printed on
+     *               stdout (in addition to be forwarded to the internal logger).
+     *
+     * @return A logger for HPACK internal debug traces
+     */
+    static Logger getHpackLogger(Supplier<String> dbgTag, Level outLevel) {
+        Level errLevel = Level.OFF;
+        return new DebugLogger(DebugLogger.HPACK, dbgTag, outLevel, errLevel);
+    }
+
+    /**
+     * Get a logger for debug HPACK traces.The logger should only be used
+     * with levels whose severity is {@code <= DEBUG}.
+     *
+     * By default, this logger will forward all messages logged to an internal
+     * logger named "jdk.internal.httpclient.hpack.debug".
+     * In addition, the provided boolean {@code on==true}, it will print the
+     * messages on stdout.
+     * The logger will add some decoration to the printed message, in the form of
+     * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+     *
+     * @apiNote To obtain a logger that will always print things on stdout in
+     *          addition to forwarding to the internal logger, use
+     *          {@code getHpackLogger(this::dbgTag, true);}.
+     *          This is also equivalent to calling
+     *          {@code getHpackLogger(this::dbgTag, Level.ALL);}.
+     *          To obtain a logger that will only forward to the internal logger,
+     *          use {@code getHpackLogger(this::dbgTag, false);}.
+     *          This is also equivalent to calling
+     *          {@code getHpackLogger(this::dbgTag, Level.OFF);}.
+     *
+     * @param dbgTag A lambda that returns a string that identifies the caller
+     *               (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
+     * @param on  Whether messages should also be printed on
+     *            stdout (in addition to be forwarded to the internal logger).
+     *
+     * @return A logger for HPACK internal debug traces
+     */
+    public static Logger getHpackLogger(Supplier<String> dbgTag, boolean on) {
+        Level outLevel = on ? Level.ALL : Level.OFF;
+        return getHpackLogger(dbgTag, outLevel);
+    }
+
+
+
+    private static final class DebugLogger implements System.Logger {
+
+        // deliberately not in the same subtree than standard loggers.
+        final static String HTTP_NAME  = "jdk.internal.httpclient.debug";
+        final static String HPACK_NAME = "jdk.internal.httpclient.hpack.debug";
+        final static Logger HTTP = System.getLogger(HTTP_NAME);
+        final static Logger HPACK = System.getLogger(HPACK_NAME);
+        final static long START_NANOS = System.nanoTime();
+
+        final Supplier<String> dbgTag;
+        final Level errLevel;
+        final Level outLevel;
+        final Logger logger;
+        final boolean debugOn;
+        final boolean traceOn;
+
+        DebugLogger(Logger logger,
+                    Supplier<String> dbgTag,
+                    Level outLevel,
+                    Level errLevel) {
+            this.dbgTag = dbgTag;
+            this.errLevel = errLevel;
+            this.outLevel = outLevel;
+            this.logger = logger;
+            // support only static configuration.
+            this.debugOn = isEnabled(Level.DEBUG);
+            this.traceOn = isEnabled(Level.TRACE);
+        }
+
+        @Override
+        public String getName() {
+            return logger.getName();
+        }
+
+        private boolean isEnabled(Level level) {
+            if (level == Level.OFF) return false;
+            int severity = level.getSeverity();
+            return severity >= errLevel.getSeverity()
+                    || severity >= outLevel.getSeverity()
+                    || logger.isLoggable(level);
+        }
+
+        @Override
+        public boolean isLoggable(Level level) {
+            // fast path, we assume these guys never change.
+            // support only static configuration.
+            if (level == Level.DEBUG) return debugOn;
+            if (level == Level.TRACE) return traceOn;
+            return isEnabled(level);
+        }
+
+        @Override
+        public void log(Level level, ResourceBundle unused,
+                        String format, Object... params) {
+            // fast path, we assume these guys never change.
+            // support only static configuration.
+            if (level == Level.DEBUG && !debugOn) return;
+            if (level == Level.TRACE && !traceOn) return;
+
+            int severity = level.getSeverity();
+            if (errLevel != Level.OFF
+                    && errLevel.getSeverity() <= severity) {
+                print(System.err, level, format, params, null);
+            }
+            if (outLevel != Level.OFF
+                    && outLevel.getSeverity() <= severity) {
+                print(System.out, level, format, params, null);
+            }
+            if (logger.isLoggable(level)) {
+                logger.log(level, unused,
+                           getFormat(new StringBuilder(), format, params).toString(),
+                           params);
+            }
+        }
+
+        @Override
+        public void log(Level level, ResourceBundle unused, String msg,
+                        Throwable thrown) {
+            // fast path, we assume these guys never change.
+            if (level == Level.DEBUG && !debugOn) return;
+            if (level == Level.TRACE && !traceOn) return;
+
+            if (errLevel != Level.OFF
+                    && errLevel.getSeverity() <= level.getSeverity()) {
+                print(System.err, level, msg, null, thrown);
+            }
+            if (outLevel != Level.OFF
+                    && outLevel.getSeverity() <= level.getSeverity()) {
+                print(System.out, level, msg, null, thrown);
+            }
+            if (logger.isLoggable(level)) {
+                logger.log(level, unused,
+                           getFormat(new StringBuilder(), msg, null).toString(),
+                           thrown);
+            }
+        }
+
+        private void print(PrintStream out, Level level, String msg,
+                           Object[] params, Throwable t) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(level.name()).append(':').append(' ');
+            sb = format(sb, msg, params);
+            if (t != null) sb.append(' ').append(t.toString());
+            out.println(sb.toString());
+            if (t != null) {
+                t.printStackTrace(out);
+            }
+        }
+
+        private StringBuilder decorate(StringBuilder sb, String msg) {
+            String tag = dbgTag == null ? null : dbgTag.get();
+            String res = msg == null ? "" : msg;
+            long elapsed = System.nanoTime() - START_NANOS;
+            long nanos =  elapsed % 1000_000;
+            long millis = elapsed / 1000_000;
+            long secs   = millis / 1000;
+            sb.append('[').append(Thread.currentThread().getName()).append(']')
+                    .append(' ').append('[');
+            if (secs > 0) {
+                sb.append(secs).append('s');
+            }
+            millis = millis % 1000;
+            if (millis > 0) {
+                if (secs > 0) sb.append(' ');
+                sb.append(millis).append("ms");
+            }
+            sb.append(']').append(' ');
+            if (tag != null) {
+                sb.append(tag).append(' ');
+            }
+            sb.append(res);
+            return sb;
+        }
+
+
+        private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) {
+            if (format == null || params == null || params.length == 0) {
+                return decorate(sb, format);
+            } else if (format.contains("{0}") || format.contains("{1}")) {
+                return decorate(sb, format);
+            } else if (format.contains("%s") || format.contains("%d")) {
+                try {
+                    return decorate(sb, String.format(format, params));
+                } catch (Throwable t) {
+                    return decorate(sb, format);
+                }
+            } else {
+                return decorate(sb, format);
+            }
+        }
+
+        private StringBuilder format(StringBuilder sb, String format, Object[] params) {
+            if (format == null || params == null || params.length == 0) {
+                return decorate(sb, format);
+            } else if (format.contains("{0}") || format.contains("{1}")) {
+                return decorate(sb, java.text.MessageFormat.format(format, params));
+            } else if (format.contains("%s") || format.contains("%d")) {
+                try {
+                    return decorate(sb, String.format(format, params));
+                } catch (Throwable t) {
+                    return decorate(sb, format);
+                }
+            } else {
+                return decorate(sb, format);
+            }
+        }
+
+    }
 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/DataFrame.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/DataFrame.java	Sun Nov 05 17:32:13 2017 +0000
@@ -47,7 +47,7 @@
     public DataFrame(int streamid, int flags, ByteBufferReference[] data) {
         super(streamid, flags);
         this.data = data;
-        this.dataLength = Utils.remaining(data);
+        this.dataLength = Utils.remaining(data, Integer.MAX_VALUE);
     }
 
     public DataFrame(int streamid, int flags, ByteBufferReference[] data, int padLength) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/FramesDecoder.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/FramesDecoder.java	Sun Nov 05 17:32:13 2017 +0000
@@ -30,6 +30,7 @@
 import jdk.incubator.http.internal.common.Utils;
 
 import java.io.IOException;
+import java.lang.System.Logger.Level;
 import java.nio.ByteBuffer;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -46,7 +47,9 @@
  */
 public class FramesDecoder {
 
-
+    static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+    static final System.Logger DEBUG_LOGGER =
+            Utils.getDebugLogger("FramesDecoder"::toString, DEBUG);
 
     @FunctionalInterface
     public interface FrameProcessor {
@@ -92,25 +95,54 @@
         this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1);
     }
 
+    /** Threshold beyond which data is no longer copied into the current buffer,
+     * if that buffer has enough unused space. */
+    private static final int COPY_THRESHOLD = 8192;
+
     /**
-     * put next buffer into queue,
-     * if frame decoding is possible - decode all buffers and invoke FrameProcessor
+     * Adds the data from the given buffer, and performs frame decoding if
+     * possible.   Either 1) appends the data from the given buffer to the
+     * current buffer ( if there is enough unused space ), or 2) adds it to the
+     * next buffer in the queue.
      *
-     * @param buffer
-     * @throws IOException
+     * If there is enough data to perform frame decoding then, all buffers are
+     * decoded and the FrameProcessor is invoked.
      */
     public void decode(ByteBufferReference buffer) throws IOException {
         int remaining = buffer.get().remaining();
+        DEBUG_LOGGER.log(Level.DEBUG, "decodes: %d", remaining);
         if (remaining > 0) {
             if (currentBuffer == null) {
                 currentBuffer = buffer;
             } else {
-                tailBuffers.add(buffer);
-                tailSize += remaining;
+                ByteBuffer cb = currentBuffer.get();
+                int freeSpace = cb.capacity() - cb.limit();
+                if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
+                    // append the new data to the unused space in the current buffer
+                    ByteBuffer b = buffer.get();
+                    int position = cb.position();
+                    int limit = cb.limit();
+                    cb.position(limit);
+                    cb.limit(limit + b.limit());
+                    cb.put(buffer.get());
+                    cb.position(position);
+                    buffer.clear();  // release the buffer, if it is a member of a pool
+                    DEBUG_LOGGER.log(Level.DEBUG, "copied: %d", remaining);
+                } else {
+                    DEBUG_LOGGER.log(Level.DEBUG, "added: %d", remaining);
+                    tailBuffers.add(buffer);
+                    tailSize += remaining;
+                }
             }
         }
+        DEBUG_LOGGER.log(Level.DEBUG, "Tail size is now: %d, current=",
+                tailSize,
+                (currentBuffer == null ? 0 :
+                  (currentBuffer.get() == null ? 0 :
+                   currentBuffer.get().remaining())));
         Http2Frame frame;
         while ((frame = nextFrame()) != null) {
+            DEBUG_LOGGER.log(Level.DEBUG, "Got frame: %s", frame);
             frameProcessor.processFrame(frame);
             frameProcessed();
         }
@@ -121,21 +153,28 @@
             if (currentBuffer == null) {
                 return null; // no data at all
             }
+            long available = currentBuffer.get().remaining() + tailSize;
             if (!frameHeaderParsed) {
-                if (currentBuffer.get().remaining() + tailSize >= Http2Frame.FRAME_HEADER_SIZE) {
+                if (available >= Http2Frame.FRAME_HEADER_SIZE) {
                     parseFrameHeader();
                     if (frameLength > maxFrameSize) {
                         // connection error
                         return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
-                                "Frame type("+frameType+") " +"length("+frameLength+") exceeds MAX_FRAME_SIZE("+ maxFrameSize+")");
+                                "Frame type("+frameType+") "
+                                +"length("+frameLength
+                                +") exceeds MAX_FRAME_SIZE("
+                                + maxFrameSize+")");
                     }
                     frameHeaderParsed = true;
                 } else {
-                    return null; // no data for frame header
+                    DEBUG_LOGGER.log(Level.DEBUG,
+                            "Not enough data to parse header, needs: %d, has: %d",
+                            Http2Frame.FRAME_HEADER_SIZE, available);
                 }
             }
+            available = currentBuffer == null ? 0 : currentBuffer.get().remaining() + tailSize;
             if ((frameLength == 0) ||
-                    (currentBuffer != null && currentBuffer.get().remaining() + tailSize >= frameLength)) {
+                    (currentBuffer != null && available >= frameLength)) {
                 Http2Frame frame = parseFrameBody();
                 frameHeaderParsed = false;
                 // frame == null means we have to skip this frame and try parse next
@@ -143,6 +182,9 @@
                     return frame;
                 }
             } else {
+                DEBUG_LOGGER.log(Level.DEBUG,
+                        "Not enough data to parse frame body, needs: %d,  has: %d",
+                        frameLength, available);
                 return null;  // no data for the whole frame header
             }
         }
@@ -296,12 +338,13 @@
     private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) {
         // non-zero stream
         if (streamid == 0) {
-            return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, "zero streamId for DataFrame");
+            return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+                                      "zero streamId for DataFrame");
         }
         int padLength = 0;
         if ((flags & DataFrame.PADDED) != 0) {
             padLength = getByte();
-            if(padLength >= frameLength) {
+            if (padLength >= frameLength) {
                 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
                         "the length of the padding is the length of the frame payload or greater");
             }
@@ -317,7 +360,8 @@
     private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) {
         // non-zero stream
         if (streamid == 0) {
-            return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, "zero streamId for HeadersFrame");
+            return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+                                      "zero streamId for HeadersFrame");
         }
         int padLength = 0;
         if ((flags & HeadersFrame.PADDED) != 0) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/HeaderFrame.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/frame/HeaderFrame.java	Sun Nov 05 17:32:13 2017 +0000
@@ -48,7 +48,7 @@
     public HeaderFrame(int streamid, int flags, ByteBufferReference[] headerBlocks) {
         super(streamid, flags);
         this.headerBlocks = headerBlocks;
-        this.headerLength = Utils.remaining(headerBlocks);
+        this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE);
     }
 
     @Override
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Decoder.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Decoder.java	Sun Nov 05 17:32:13 2017 +0000
@@ -24,20 +24,22 @@
  */
 package jdk.incubator.http.internal.hpack;
 
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
 import jdk.internal.vm.annotation.Stable;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.ProtocolException;
 import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicLong;
 
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
 import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
 
 /**
  * Decodes headers from their binary representation.
  *
- * <p>Typical lifecycle looks like this:
+ * <p> Typical lifecycle looks like this:
  *
  * <p> {@link #Decoder(int) new Decoder}
  * ({@link #setMaxCapacity(int) setMaxCapacity}?
@@ -62,6 +64,9 @@
  */
 public final class Decoder {
 
+    private final Logger logger;
+    private static final AtomicLong DECODERS_IDS = new AtomicLong();
+
     @Stable
     private static final State[] states = new State[256];
 
@@ -92,6 +97,7 @@
         }
     }
 
+    private final long id;
     private final HeaderTable table;
 
     private State state = State.READY;
@@ -111,9 +117,8 @@
      * header table.
      *
      * <p> The value has to be agreed between decoder and encoder out-of-band,
-     * e.g. by a protocol that uses HPACK (see <a
-     * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
-     * Size</a>).
+     * e.g. by a protocol that uses HPACK
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
      *
      * @param capacity
      *         a non-negative integer
@@ -122,8 +127,24 @@
      *         if capacity is negative
      */
     public Decoder(int capacity) {
-        setMaxCapacity(capacity);
-        table = new HeaderTable(capacity);
+        id = DECODERS_IDS.incrementAndGet();
+        logger = HPACK.getLogger().subLogger("Decoder#" + id);
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("new decoder with maximum table size %s",
+                                            capacity));
+        }
+        if (logger.isLoggable(NORMAL)) {
+            /* To correlate with logging outside HPACK, knowing
+               hashCode/toString is important */
+            logger.log(NORMAL, () -> {
+                String hashCode = Integer.toHexString(
+                        System.identityHashCode(this));
+                return format("toString='%s', identityHashCode=%s",
+                              toString(), hashCode);
+            });
+        }
+        setMaxCapacity0(capacity);
+        table = new HeaderTable(capacity, logger.subLogger("HeaderTable"));
         integerReader = new IntegerReader();
         stringReader = new StringReader();
         name = new StringBuilder(512);
@@ -134,9 +155,8 @@
      * Sets a maximum capacity of the header table.
      *
      * <p> The value has to be agreed between decoder and encoder out-of-band,
-     * e.g. by a protocol that uses HPACK (see <a
-     * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
-     * Size</a>).
+     * e.g. by a protocol that uses HPACK
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
      *
      * @param capacity
      *         a non-negative integer
@@ -145,6 +165,14 @@
      *         if capacity is negative
      */
     public void setMaxCapacity(int capacity) {
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("setting maximum table size to %s",
+                                            capacity));
+        }
+        setMaxCapacity0(capacity);
+    }
+
+    private void setMaxCapacity0(int capacity) {
         if (capacity < 0) {
             throw new IllegalArgumentException("capacity >= 0: " + capacity);
         }
@@ -155,8 +183,8 @@
     /**
      * Decodes a header block from the given buffer to the given callback.
      *
-     * <p> Suppose a header block is represented by a sequence of {@code
-     * ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
+     * <p> Suppose a header block is represented by a sequence of
+     * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
      * consumer of decoded headers is represented by the callback. Then to
      * decode the header block, the following approach might be used:
      *
@@ -174,7 +202,7 @@
      *
      * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the
      * current header block is deemed ended, and inconsistencies, if any, are
-     * reported immediately by throwing an {@code UncheckedIOException}.
+     * reported immediately by throwing an {@code IOException}.
      *
      * <p> Each callback method is called only after the implementation has
      * processed the corresponding bytes. If the bytes revealed a decoding
@@ -200,25 +228,32 @@
      *
      * @param consumer
      *         the callback
-     * @throws UncheckedIOException
+     * @throws IOException
      *         in case of a decoding error
      * @throws NullPointerException
      *         if either headerBlock or consumer are null
      */
-    public void decode(ByteBuffer headerBlock, boolean endOfHeaderBlock,
-                       DecodingCallback consumer) {
+    public void decode(ByteBuffer headerBlock,
+                       boolean endOfHeaderBlock,
+                       DecodingCallback consumer) throws IOException {
         requireNonNull(headerBlock, "headerBlock");
         requireNonNull(consumer, "consumer");
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
+                                            headerBlock, endOfHeaderBlock));
+        }
         while (headerBlock.hasRemaining()) {
             proceed(headerBlock, consumer);
         }
         if (endOfHeaderBlock && state != State.READY) {
-            throw new UncheckedIOException(
-                    new ProtocolException("Unexpected end of header block"));
+            logger.log(NORMAL, () -> format("unexpected end of %s representation",
+                                            state));
+            throw new IOException("Unexpected end of header block");
         }
     }
 
-    private void proceed(ByteBuffer input, DecodingCallback action) {
+    private void proceed(ByteBuffer input, DecodingCallback action)
+            throws IOException {
         switch (state) {
             case READY:
                 resumeReady(input);
@@ -239,14 +274,17 @@
                 resumeSizeUpdate(input, action);
                 break;
             default:
-                throw new InternalError(
-                        "Unexpected decoder state: " + String.valueOf(state));
+                throw new InternalError("Unexpected decoder state: " + state);
         }
     }
 
     private void resumeReady(ByteBuffer input) {
         int b = input.get(input.position()) & 0xff; // absolute read
         State s = states[b];
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
+                                           s, b));
+        }
         switch (s) {
             case INDEXED:
                 integerReader.configure(7);
@@ -292,20 +330,36 @@
     //            | 1 |        Index (7+)         |
     //            +---+---------------------------+
     //
-    private void resumeIndexed(ByteBuffer input, DecodingCallback action) {
+    private void resumeIndexed(ByteBuffer input, DecodingCallback action)
+            throws IOException {
         if (!integerReader.read(input)) {
             return;
         }
         intValue = integerReader.get();
         integerReader.reset();
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("indexed %s", intValue));
+        }
         try {
-            HeaderTable.HeaderField f = table.get(intValue);
+            HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
             action.onIndexed(intValue, f.name, f.value);
         } finally {
             state = State.READY;
         }
     }
 
+    private HeaderTable.HeaderField getHeaderFieldAt(int index)
+            throws IOException
+    {
+        HeaderTable.HeaderField f;
+        try {
+            f = table.get(index);
+        } catch (IndexOutOfBoundsException e) {
+            throw new IOException("header fields table index", e);
+        }
+        return f;
+    }
+
     //              0   1   2   3   4   5   6   7
     //            +---+---+---+---+---+---+---+---+
     //            | 0 | 0 | 0 | 0 |  Index (4+)   |
@@ -328,15 +382,24 @@
     //            | Value String (Length octets)  |
     //            +-------------------------------+
     //
-    private void resumeLiteral(ByteBuffer input, DecodingCallback action) {
+    private void resumeLiteral(ByteBuffer input, DecodingCallback action)
+            throws IOException {
         if (!completeReading(input)) {
             return;
         }
         try {
             if (firstValueIndex) {
-                HeaderTable.HeaderField f = table.get(intValue);
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
+                                                    intValue, value));
+                }
+                HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
                 action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
             } else {
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
+                                                    name, value));
+                }
                 action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
             }
         } finally {
@@ -367,7 +430,9 @@
     //            | Value String (Length octets)  |
     //            +-------------------------------+
     //
-    private void resumeLiteralWithIndexing(ByteBuffer input, DecodingCallback action) {
+    private void resumeLiteralWithIndexing(ByteBuffer input,
+                                           DecodingCallback action)
+            throws IOException {
         if (!completeReading(input)) {
             return;
         }
@@ -381,17 +446,22 @@
             String n;
             String v = value.toString();
             if (firstValueIndex) {
-                HeaderTable.HeaderField f = table.get(intValue);
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
+                                                    intValue, value));
+                }
+                HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
                 n = f.name;
                 action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded);
             } else {
                 n = name.toString();
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
+                                                    n, value));
+                }
                 action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
             }
             table.put(n, v);
-        } catch (IllegalArgumentException | IllegalStateException e) {
-            throw new UncheckedIOException(
-                    (IOException) new ProtocolException().initCause(e));
         } finally {
             cleanUpAfterReading();
         }
@@ -419,15 +489,25 @@
     //            | Value String (Length octets)  |
     //            +-------------------------------+
     //
-    private void resumeLiteralNeverIndexed(ByteBuffer input, DecodingCallback action) {
+    private void resumeLiteralNeverIndexed(ByteBuffer input,
+                                           DecodingCallback action)
+            throws IOException {
         if (!completeReading(input)) {
             return;
         }
         try {
             if (firstValueIndex) {
-                HeaderTable.HeaderField f = table.get(intValue);
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
+                                                    intValue, value));
+                }
+                HeaderTable.HeaderField f = getHeaderFieldAt(intValue);
                 action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
             } else {
+                if (logger.isLoggable(NORMAL)) {
+                    logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
+                                                    name, value));
+                }
                 action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
             }
         } finally {
@@ -440,16 +520,21 @@
     //            | 0 | 0 | 1 |   Max size (5+)   |
     //            +---+---------------------------+
     //
-    private void resumeSizeUpdate(ByteBuffer input, DecodingCallback action) {
+    private void resumeSizeUpdate(ByteBuffer input,
+                                  DecodingCallback action) throws IOException {
         if (!integerReader.read(input)) {
             return;
         }
         intValue = integerReader.get();
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("dynamic table size update %s",
+                                            intValue));
+        }
         assert intValue >= 0;
         if (intValue > capacity) {
-            throw new UncheckedIOException(new ProtocolException(
-                    format("Received capacity exceeds expected: " +
-                            "capacity=%s, expected=%s", intValue, capacity)));
+            throw new IOException(
+                    format("Received capacity exceeds expected: capacity=%s, expected=%s",
+                           intValue, capacity));
         }
         integerReader.reset();
         try {
@@ -460,7 +545,7 @@
         }
     }
 
-    private boolean completeReading(ByteBuffer input) {
+    private boolean completeReading(ByteBuffer input) throws IOException {
         if (!firstValueRead) {
             if (firstValueIndex) {
                 if (!integerReader.read(input)) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/DecodingCallback.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/DecodingCallback.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -35,10 +35,10 @@
  *
  * @apiNote
  *
- * <p> The callback provides methods for all possible <a
- * href="https://tools.ietf.org/html/rfc7541#section-6">binary
- * representations</a>. This could be useful for implementing an intermediary,
- * logging, debugging, etc.
+ * <p> The callback provides methods for all possible
+ * <a href="https://tools.ietf.org/html/rfc7541#section-6">binary representations</a>.
+ * This could be useful for implementing an intermediary, logging, debugging,
+ * etc.
  *
  * <p> The callback is an interface in order to interoperate with lambdas (in
  * the most common use case):
@@ -98,7 +98,8 @@
      * @see #onLiteralNeverIndexed(int, CharSequence, CharSequence, boolean)
      * @see #onLiteralNeverIndexed(CharSequence, boolean, CharSequence, boolean)
      */
-    default void onDecoded(CharSequence name, CharSequence value,
+    default void onDecoded(CharSequence name,
+                           CharSequence value,
                            boolean sensitive) {
         onDecoded(name, value);
     }
@@ -142,8 +143,10 @@
      * @param valueHuffman
      *         if the {@code value} was Huffman encoded
      */
-    default void onLiteral(int index, CharSequence name,
-                           CharSequence value, boolean valueHuffman) {
+    default void onLiteral(int index,
+                           CharSequence name,
+                           CharSequence value,
+                           boolean valueHuffman) {
         onDecoded(name, value, false);
     }
 
@@ -166,8 +169,10 @@
      * @param valueHuffman
      *         if the {@code value} was Huffman encoded
      */
-    default void onLiteral(CharSequence name, boolean nameHuffman,
-                           CharSequence value, boolean valueHuffman) {
+    default void onLiteral(CharSequence name,
+                           boolean nameHuffman,
+                           CharSequence value,
+                           boolean valueHuffman) {
         onDecoded(name, value, false);
     }
 
@@ -190,7 +195,8 @@
      * @param valueHuffman
      *         if the {@code value} was Huffman encoded
      */
-    default void onLiteralNeverIndexed(int index, CharSequence name,
+    default void onLiteralNeverIndexed(int index,
+                                       CharSequence name,
                                        CharSequence value,
                                        boolean valueHuffman) {
         onDecoded(name, value, true);
@@ -215,8 +221,10 @@
      * @param valueHuffman
      *         if the {@code value} was Huffman encoded
      */
-    default void onLiteralNeverIndexed(CharSequence name, boolean nameHuffman,
-                                       CharSequence value, boolean valueHuffman) {
+    default void onLiteralNeverIndexed(CharSequence name,
+                                       boolean nameHuffman,
+                                       CharSequence value,
+                                       boolean valueHuffman) {
         onDecoded(name, value, true);
     }
 
@@ -241,7 +249,8 @@
      */
     default void onLiteralWithIndexing(int index,
                                        CharSequence name,
-                                       CharSequence value, boolean valueHuffman) {
+                                       CharSequence value,
+                                       boolean valueHuffman) {
         onDecoded(name, value, false);
     }
 
@@ -264,8 +273,10 @@
      * @param valueHuffman
      *         if the {@code value} was Huffman encoded
      */
-    default void onLiteralWithIndexing(CharSequence name, boolean nameHuffman,
-                                       CharSequence value, boolean valueHuffman) {
+    default void onLiteralWithIndexing(CharSequence name,
+                                       boolean nameHuffman,
+                                       CharSequence value,
+                                       boolean valueHuffman) {
         onDecoded(name, value, false);
     }
 
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -24,27 +24,32 @@
  */
 package jdk.incubator.http.internal.hpack;
 
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
+
 import java.nio.ByteBuffer;
 import java.nio.ReadOnlyBufferException;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
 
 import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
 
 /**
  * Encodes headers to their binary representation.
  *
- * <p>Typical lifecycle looks like this:
+ * <p> Typical lifecycle looks like this:
  *
  * <p> {@link #Encoder(int) new Encoder}
  * ({@link #setMaxCapacity(int) setMaxCapacity}?
  * {@link #encode(ByteBuffer) encode})*
  *
- * <p> Suppose headers are represented by {@code Map<String, List<String>>}. A
- * supplier and a consumer of {@link ByteBuffer}s in forms of {@code
- * Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively. Then to
- * encode headers, the following approach might be used:
+ * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
+ * A supplier and a consumer of {@link ByteBuffer}s in forms of
+ * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
+ * Then to encode headers, the following approach might be used:
  *
  * <pre>{@code
  *     for (Map.Entry<String, List<String>> h : headers.entrySet()) {
@@ -61,10 +66,9 @@
  *     }
  * }</pre>
  *
- * <p> Though the specification <a
- * href="https://tools.ietf.org/html/rfc7541#section-2"> does not define</a> how
- * an encoder is to be implemented, a default implementation is provided by the
- * method {@link #header(CharSequence, CharSequence, boolean)}.
+ * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
+ * how an encoder is to be implemented, a default implementation is provided by
+ * the method {@link #header(CharSequence, CharSequence, boolean)}.
  *
  * <p> To provide a custom encoding implementation, {@code Encoder} has to be
  * extended. A subclass then can access methods for encoding using specific
@@ -85,8 +89,8 @@
  * the resulting header block afterwards.
  *
  * <p> Splitting the encoding operation into header set up and header encoding,
- * separates long lived arguments ({@code name}, {@code value}, {@code
- * sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
+ * separates long lived arguments ({@code name}, {@code value},
+ * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
  * simplifying each operation itself.
  *
  * @implNote
@@ -99,9 +103,13 @@
  */
 public class Encoder {
 
+    private static final AtomicLong ENCODERS_IDS = new AtomicLong();
+
     // TODO: enum: no huffman/smart huffman/always huffman
     private static final boolean DEFAULT_HUFFMAN = true;
 
+    private final Logger logger;
+    private final long id;
     private final IndexedWriter indexedWriter = new IndexedWriter();
     private final LiteralWriter literalWriter = new LiteralWriter();
     private final LiteralNeverIndexedWriter literalNeverIndexedWriter
@@ -129,9 +137,8 @@
      * header table.
      *
      * <p> The value has to be agreed between decoder and encoder out-of-band,
-     * e.g. by a protocol that uses HPACK (see <a
-     * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
-     * Size</a>).
+     * e.g. by a protocol that uses HPACK
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
      *
      * @param maxCapacity
      *         a non-negative integer
@@ -140,14 +147,33 @@
      *         if maxCapacity is negative
      */
     public Encoder(int maxCapacity) {
+        id = ENCODERS_IDS.incrementAndGet();
+        this.logger = HPACK.getLogger().subLogger("Encoder#" + id);
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("new encoder with maximum table size %s",
+                                            maxCapacity));
+        }
+        if (logger.isLoggable(EXTRA)) {
+            /* To correlate with logging outside HPACK, knowing
+               hashCode/toString is important */
+            logger.log(EXTRA, () -> {
+                String hashCode = Integer.toHexString(
+                        System.identityHashCode(this));
+                /* Since Encoder can be subclassed hashCode AND identity
+                   hashCode might be different. So let's print both. */
+                return format("toString='%s', hashCode=%s, identityHashCode=%s",
+                              toString(), hashCode(), hashCode);
+            });
+        }
         if (maxCapacity < 0) {
-            throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity);
+            throw new IllegalArgumentException(
+                    "maxCapacity >= 0: " + maxCapacity);
         }
         // Initial maximum capacity update mechanics
         minCapacity = Long.MAX_VALUE;
         currCapacity = -1;
-        setMaxCapacity(maxCapacity);
-        headerTable = new HeaderTable(lastCapacity);
+        setMaxCapacity0(maxCapacity);
+        headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable"));
     }
 
     /**
@@ -176,6 +202,10 @@
      * Sets up the given header {@code (name, value)} with possibly sensitive
      * value.
      *
+     * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+     * this encoder will compress it using a special representation
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3.  Literal Header Field Never Indexed</a>).
+     *
      * <p> Fixates {@code name} and {@code value} for the duration of encoding.
      *
      * @param name
@@ -193,8 +223,13 @@
      * @see #header(CharSequence, CharSequence)
      * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
      */
-    public void header(CharSequence name, CharSequence value,
+    public void header(CharSequence name,
+                       CharSequence value,
                        boolean sensitive) throws IllegalStateException {
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s",
+                                            name, value, sensitive));
+        }
         // Arguably a good balance between complexity of implementation and
         // efficiency of encoding
         requireNonNull(name, "name");
@@ -222,9 +257,8 @@
      * Sets a maximum capacity of the header table.
      *
      * <p> The value has to be agreed between decoder and encoder out-of-band,
-     * e.g. by a protocol that uses HPACK (see <a
-     * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table
-     * Size</a>).
+     * e.g. by a protocol that uses HPACK
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
      *
      * <p> May be called any number of times after or before a complete header
      * has been encoded.
@@ -242,11 +276,23 @@
      *         hasn't yet started to encode it
      */
     public void setMaxCapacity(int capacity) {
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("setting maximum table size to %s",
+                                            capacity));
+        }
+        setMaxCapacity0(capacity);
+    }
+
+    private void setMaxCapacity0(int capacity) {
         checkEncoding();
         if (capacity < 0) {
             throw new IllegalArgumentException("capacity >= 0: " + capacity);
         }
         int calculated = calculateCapacity(capacity);
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("actual maximum table size will be %s",
+                                            calculated));
+        }
         if (calculated < 0 || calculated > capacity) {
             throw new IllegalArgumentException(
                     format("0 <= calculated <= capacity: calculated=%s, capacity=%s",
@@ -263,9 +309,22 @@
         minCapacity = Math.min(minCapacity, lastCapacity);
     }
 
+    /**
+     * Calculates actual capacity to be used by this encoder in response to
+     * a request to update maximum table size.
+     *
+     * <p> Default implementation does not add anything to the headers table,
+     * hence this method returns {@code 0}.
+     *
+     * <p> It is an error to return a value {@code c}, where {@code c < 0} or
+     * {@code c > maxCapacity}.
+     *
+     * @param maxCapacity
+     *         upper bound
+     *
+     * @return actual capacity
+     */
     protected int calculateCapacity(int maxCapacity) {
-        // Default implementation of the Encoder won't add anything to the
-        // table, therefore no need for a table space
         return 0;
     }
 
@@ -298,7 +357,10 @@
         if (!encoding) {
             throw new IllegalStateException("A header hasn't been set up");
         }
-        if (!prependWithCapacityUpdate(headerBlock)) {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("writing to %s", headerBlock));
+        }
+        if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log
             return false;
         }
         boolean done = writer.write(headerTable, headerBlock);
@@ -339,21 +401,35 @@
 
     protected final void indexed(int index) throws IndexOutOfBoundsException {
         checkEncoding();
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("indexed %s", index));
+        }
         encoding = true;
         writer = indexedWriter.index(index);
     }
 
-    protected final void literal(int index, CharSequence value,
+    protected final void literal(int index,
+                                 CharSequence value,
                                  boolean useHuffman)
             throws IndexOutOfBoundsException {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+                                           index, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalWriter
                 .index(index).value(value, useHuffman);
     }
 
-    protected final void literal(CharSequence name, boolean nameHuffman,
-                                 CharSequence value, boolean valueHuffman) {
+    protected final void literal(CharSequence name,
+                                 boolean nameHuffman,
+                                 CharSequence value,
+                                 boolean valueHuffman) {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+                                           name, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalWriter
@@ -364,6 +440,10 @@
                                              CharSequence value,
                                              boolean valueHuffman)
             throws IndexOutOfBoundsException {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+                                           index, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalNeverIndexedWriter
@@ -374,6 +454,10 @@
                                              boolean nameHuffman,
                                              CharSequence value,
                                              boolean valueHuffman) {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+                                           name, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalNeverIndexedWriter
@@ -384,6 +468,10 @@
                                              CharSequence value,
                                              boolean valueHuffman)
             throws IndexOutOfBoundsException {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+                                           index, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalWithIndexingWriter
@@ -394,6 +482,10 @@
                                              boolean nameHuffman,
                                              CharSequence value,
                                              boolean valueHuffman) {
+        if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
+            logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+                                           name, value));
+        }
         checkEncoding();
         encoding = true;
         writer = literalWithIndexingWriter
@@ -402,6 +494,10 @@
 
     protected final void sizeUpdate(int capacity)
             throws IllegalArgumentException {
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("dynamic table size update %s",
+                                           capacity));
+        }
         checkEncoding();
         // Ensure subclass follows the contract
         if (capacity > this.maxCapacity) {
@@ -420,7 +516,7 @@
         return headerTable;
     }
 
-    protected final void checkEncoding() {
+    protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress()
         if (encoding) {
             throw new IllegalStateException(
                     "Previous encoding operation hasn't finished yet");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HPACK.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2017, 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.incubator.http.internal.hpack;
+
+import jdk.incubator.http.internal.hpack.HPACK.Logger.Level;
+import jdk.internal.vm.annotation.Stable;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.joining;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NONE;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
+
+/**
+ * Internal utilities and stuff.
+ */
+public final class HPACK {
+
+    private static final RootLogger LOGGER;
+    private static final Map<String, Level> logLevels =
+            Map.of("NORMAL", NORMAL, "EXTRA", EXTRA);
+
+    static {
+        String PROPERTY = "jdk.internal.httpclient.hpack.log.level";
+
+        String value = AccessController.doPrivileged(
+                (PrivilegedAction<String>) () -> System.getProperty(PROPERTY));
+
+        if (value == null) {
+            LOGGER = new RootLogger(NONE);
+        } else {
+            String upperCasedValue = value.toUpperCase();
+            Level l = logLevels.get(upperCasedValue);
+            if (l == null) {
+                LOGGER = new RootLogger(NONE);
+                LOGGER.log(() -> format("%s value '%s' is not recognized (use %s), logging is disabled",
+                                        PROPERTY, value, logLevels.keySet().stream().collect(joining(", "))));
+            } else {
+                LOGGER = new RootLogger(l);
+                LOGGER.log(() -> format("logging level is %s", l));
+            }
+        }
+    }
+
+    public static Logger getLogger() {
+        return LOGGER;
+    }
+
+    private HPACK() { }
+
+    /**
+     * The purpose of this logger is to provide means of diagnosing issues _in
+     * the HPACK implementation_. It's not a general purpose logger.
+     */
+    public static class Logger {
+
+        /**
+         * Log detail level.
+         */
+        public enum Level {
+
+            NONE(0),
+            NORMAL(1),
+            EXTRA(2);
+
+            private final int level;
+
+            Level(int i) {
+                level = i;
+            }
+
+            public final boolean implies(Level other) {
+                return this.level >= other.level;
+            }
+        }
+
+        private final String name;
+        @Stable
+        private final Logger[] path; /* A path to parent: [root, ..., parent, this] */
+
+        private Logger(Logger[] path, String name) {
+            Logger[] p = Arrays.copyOfRange(path, 0, path.length + 1);
+            p[path.length] = this;
+            this.path = p;
+            this.name = name;
+        }
+
+        protected final String getName() {
+            return name;
+        }
+        /*
+         * Usual performance trick for logging, reducing performance overhead in
+         * the case where logging with the specified level is a NOP.
+         */
+
+        public boolean isLoggable(Level level) {
+            return isLoggable(path, level);
+        }
+
+        public void log(Level level, Supplier<? extends CharSequence> s) {
+            log(path, level, s);
+        }
+
+        public Logger subLogger(String name) {
+            return new Logger(path, name);
+        }
+
+        protected boolean isLoggable(Logger[] path, Level level) {
+            return parent().isLoggable(path, level);
+        }
+
+        protected void log(Logger[] path,
+                           Level level,
+                           Supplier<? extends CharSequence> s) {
+            parent().log(path, level, s);
+        }
+
+        protected final Logger parent() {
+            return path[path.length - 2];
+        }
+    }
+
+    private static final class RootLogger extends Logger {
+
+        private final Level level;
+
+        protected RootLogger(Level level) {
+            super(new Logger[]{ }, "hpack");
+            this.level = level;
+        }
+
+        @Override
+        protected boolean isLoggable(Logger[] path, Level level) {
+            return this.level.implies(level);
+        }
+
+        @Override
+        protected void log(Logger[] path,
+                           Level level,
+                           Supplier<? extends CharSequence> s) {
+            if (this.level.implies(level)) {
+                StringBuilder b = new StringBuilder();
+                for (Logger p : path) {
+                    b.append('/').append(p.getName());
+                }
+                System.out.println(b.toString() + ' ' + s.get());
+            }
+        }
+
+        public void log(Supplier<? extends CharSequence> s) {
+            System.out.println(getName() + ' ' + s.get());
+        }
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java	Sun Nov 05 17:32:13 2017 +0000
@@ -24,6 +24,7 @@
  */
 package jdk.incubator.http.internal.hpack;
 
+import jdk.incubator.http.internal.hpack.HPACK.Logger;
 import jdk.internal.vm.annotation.Stable;
 
 import java.util.HashMap;
@@ -32,6 +33,8 @@
 import java.util.NoSuchElementException;
 
 import static java.lang.String.format;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
 
 //
 // Header Table combined from two tables: static and dynamic.
@@ -122,11 +125,13 @@
         }
     }
 
+    private final Logger logger;
     private final Table dynamicTable = new Table(0);
     private int maxSize;
     private int size;
 
-    public HeaderTable(int maxSize) {
+    public HeaderTable(int maxSize, Logger logger) {
+        this.logger = logger;
         setMaxSize(maxSize);
     }
 
@@ -211,21 +216,41 @@
     }
 
     private void put(HeaderField h) {
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("adding ('%s', '%s')",
+                                            h.name, h.value));
+        }
         int entrySize = sizeOf(h);
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s",
+                                           h.name, h.value, entrySize));
+        }
         while (entrySize > maxSize - size && size != 0) {
+            if (logger.isLoggable(EXTRA)) {
+                logger.log(EXTRA, () -> format("insufficient space %s, must evict entry",
+                                               (maxSize - size)));
+            }
             evictEntry();
         }
         if (entrySize > maxSize - size) {
+            if (logger.isLoggable(EXTRA)) {
+                logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big",
+                                               h.name, h.value));
+            }
             return;
         }
         size += entrySize;
         dynamicTable.add(h);
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value));
+            logger.log(EXTRA, this::toString);
+        }
     }
 
     void setMaxSize(int maxSize) {
         if (maxSize < 0) {
-            throw new IllegalArgumentException
-                    ("maxSize >= 0: maxSize=" + maxSize);
+            throw new IllegalArgumentException(
+                    "maxSize >= 0: maxSize=" + maxSize);
         }
         while (maxSize < size && size != 0) {
             evictEntry();
@@ -237,22 +262,29 @@
 
     HeaderField evictEntry() {
         HeaderField f = dynamicTable.remove();
-        size -= sizeOf(f);
+        int s = sizeOf(f);
+        this.size -= s;
+        if (logger.isLoggable(EXTRA)) {
+            logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s",
+                                           f.name, f.value, s));
+            logger.log(EXTRA, this::toString);
+        }
         return f;
     }
 
     @Override
     public String toString() {
         double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize);
-        return format("entries: %d; used %s/%s (%.1f%%)", dynamicTable.size(),
-                size, maxSize, used);
+        return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)",
+                      dynamicTable.size(), length(), size, maxSize, used);
     }
 
-    int checkIndex(int index) {
-        if (index < 1 || index > STATIC_TABLE_LENGTH + dynamicTable.size()) {
-            throw new IllegalArgumentException(
+    private int checkIndex(int index) {
+        int len = length();
+        if (index < 1 || index > len) {
+            throw new IndexOutOfBoundsException(
                     format("1 <= index <= length(): index=%s, length()=%s",
-                            index, length()));
+                           index, len));
         }
         return index;
     }
@@ -272,7 +304,7 @@
         StringBuilder b = new StringBuilder();
         for (int i = 1, size = dynamicTable.size(); i <= size; i++) {
             HeaderField e = dynamicTable.get(i);
-            b.append(format("[%3d] (s = %3d) %s: %s\n", i,
+            b.append(format("[%3d] (s = %3d) %s: %s%n", i,
                     sizeOf(e), e.name, e.value));
         }
         b.append(format("      Table size:%4s", this.size));
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Huffman.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Huffman.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -25,7 +25,6 @@
 package jdk.incubator.http.internal.hpack;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 
 import static java.lang.String.format;
@@ -51,16 +50,18 @@
             reset();
         }
 
-        public void read(ByteBuffer source, Appendable destination,
-                         boolean isLast) {
+        public void read(ByteBuffer source,
+                         Appendable destination,
+                         boolean isLast) throws IOException {
             read(source, destination, true, isLast);
         }
 
         // Takes 'isLast' rather than returns whether the reading is done or
         // not, for more informative exceptions.
-        void read(ByteBuffer source, Appendable destination, boolean reportEOS,
-                  boolean isLast) {
-
+        void read(ByteBuffer source,
+                  Appendable destination,
+                  boolean reportEOS, /* reportEOS is exposed for tests */
+                  boolean isLast) throws IOException {
             Node c = curr;
             int l = len;
             /*
@@ -77,16 +78,20 @@
                     l++;
                     if (c.isLeaf()) {
                         if (reportEOS && c.isEOSPath) {
-                            throw new IllegalArgumentException("Encountered EOS");
+                            throw new IOException("Encountered EOS");
+                        }
+                        char ch;
+                        try {
+                            ch = c.getChar();
+                        } catch (IllegalStateException e) {
+                            source.position(pos); // do we need this?
+                            throw new IOException(e);
                         }
                         try {
-                            destination.append(c.getChar());
-                        } catch (RuntimeException | Error e) {
-                            source.position(pos);
+                            destination.append(ch);
+                        } catch (IOException e) {
+                            source.position(pos); // do we need this?
                             throw e;
-                        } catch (IOException e) {
-                            source.position(pos);
-                            throw new UncheckedIOException(e);
                         }
                         c = INSTANCE.root;
                         l = 0;
@@ -107,11 +112,11 @@
                 return; // it's ok, some extra padding bits
             }
             if (c.isEOSPath) {
-                throw new IllegalArgumentException(
+                throw new IOException(
                         "Padding is too long (len=" + len + ") " +
                                 "or unexpected end of data");
             }
-            throw new IllegalArgumentException(
+            throw new IOException(
                     "Not a EOS prefix padding or unexpected end of data");
         }
 
@@ -509,8 +514,8 @@
      * @throws NullPointerException
      *         if the value is null
      * @throws IndexOutOfBoundsException
-     *         if any invocation of {@code value.charAt(i)}, where {@code start
-     *         <= i < end} would throw an IndexOutOfBoundsException
+     *         if any invocation of {@code value.charAt(i)}, where
+     *         {@code start <= i < end} would throw an IndexOutOfBoundsException
      */
     public int lengthOf(CharSequence value, int start, int end) {
         int len = 0;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/ISO_8859_1.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/ISO_8859_1.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -25,7 +25,6 @@
 package jdk.incubator.http.internal.hpack;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 
 //
@@ -47,14 +46,15 @@
 
     public static final class Reader {
 
-        public void read(ByteBuffer source, Appendable destination) {
+        public void read(ByteBuffer source, Appendable destination)
+                throws IOException {
             for (int i = 0, len = source.remaining(); i < len; i++) {
                 char c = (char) (source.get() & 0xff);
                 try {
                     destination.append(c);
                 } catch (IOException e) {
-                    throw new UncheckedIOException
-                            ("Error appending to the destination", e);
+                    throw new IOException(
+                            "Error appending to the destination", e);
                 }
             }
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IndexNameValueWriter.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IndexNameValueWriter.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -73,7 +73,8 @@
                     return false;
                 }
             } else {
-                if (!intWriter.write(destination) || !nameWriter.write(destination)) {
+                if (!intWriter.write(destination) ||
+                        !nameWriter.write(destination)) {
                     return false;
                 }
             }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IntegerReader.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/IntegerReader.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -24,6 +24,7 @@
  */
 package jdk.incubator.http.internal.hpack;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 
@@ -73,7 +74,7 @@
         return this;
     }
 
-    public boolean read(ByteBuffer input) {
+    public boolean read(ByteBuffer input) throws IOException {
         if (state == NEW) {
             throw new IllegalStateException("Configure first");
         }
@@ -105,7 +106,7 @@
                 i = input.get();
                 long increment = b * (i & 127);
                 if (r + increment > maxValue) {
-                    throw new IllegalArgumentException(format(
+                    throw new IOException(format(
                             "Integer overflow: maxValue=%,d, value=%,d",
                             maxValue, r + increment));
                 }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringReader.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringReader.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -24,6 +24,7 @@
  */
 package jdk.incubator.http.internal.hpack;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 
@@ -51,7 +52,7 @@
     private boolean huffman;
     private int remainingLength;
 
-    boolean read(ByteBuffer input, Appendable output) {
+    boolean read(ByteBuffer input, Appendable output) throws IOException {
         if (state == DONE) {
             return true;
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringWriter.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/StringWriter.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -62,7 +62,9 @@
         return configure(input, 0, input.length(), huffman);
     }
 
-    StringWriter configure(CharSequence input, int start, int end,
+    StringWriter configure(CharSequence input,
+                           int start,
+                           int end,
                            boolean huffman) {
         if (start < 0 || end < 0 || end > input.length() || start > end) {
             throw new IndexOutOfBoundsException(
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/CooperativeHandler.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,208 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.http.internal.websocket;
-
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-
-import static java.util.Objects.requireNonNull;
-
-/*
- * A synchronization aid that assists a number of parties in running a task
- * in a mutually exclusive fashion.
- *
- * To run the task, a party invokes `handle`. To permanently prevent the task
- * from subsequent runs, the party invokes `stop`.
- *
- * The parties do not have to operate in different threads.
- *
- * The task can be either synchronous or asynchronous.
- *
- * If the task is synchronous, it is represented with `Runnable`.
- * The handler invokes `Runnable.run` to run the task.
- *
- * If the task is asynchronous, it is represented with `Consumer<Runnable>`.
- * The handler invokes `Consumer.accept(end)` to begin the task. The task
- * invokes `end.run()` when it has ended.
- *
- * The next run of the task will not begin until the previous run has finished.
- *
- * The task may invoke `handle()` by itself, it's a normal situation.
- */
-public final class CooperativeHandler {
-
-    /*
-       Since the task is fixed and known beforehand, no blocking synchronization
-       (locks, queues, etc.) is required. The job can be done solely using
-       nonblocking primitives.
-
-       The machinery below addresses two problems:
-
-         1. Running the task in a sequential order (no concurrent runs):
-
-                begin, end, begin, end...
-
-         2. Avoiding indefinite recursion:
-
-                begin
-                  end
-                    begin
-                      end
-                        ...
-
-       Problem #1 is solved with a finite state machine with 4 states:
-
-           BEGIN, AGAIN, END, and STOP.
-
-       Problem #2 is solved with a "state modifier" OFFLOAD.
-
-       Parties invoke `handle()` to signal the task must run. A party that has
-       invoked `handle()` either begins the task or exploits the party that is
-       either beginning the task or ending it.
-
-       The party that is trying to end the task either ends it or begins it
-       again.
-
-       To avoid indefinite recursion, before re-running the task tryEnd() sets
-       OFFLOAD bit, signalling to its "child" tryEnd() that this ("parent")
-       tryEnd() is available and the "child" must offload the task on to the
-       "parent". Then a race begins. Whichever invocation of tryEnd() manages
-       to unset OFFLOAD bit first does not do the work.
-
-       There is at most 1 thread that is beginning the task and at most 2
-       threads that are trying to end it: "parent" and "child". In case of a
-       synchronous task "parent" and "child" are the same thread.
-     */
-
-    private static final int OFFLOAD =  1;
-    private static final int AGAIN   =  2;
-    private static final int BEGIN   =  4;
-    private static final int STOP    =  8;
-    private static final int END     = 16;
-
-    private final AtomicInteger state = new AtomicInteger(END);
-    private final Consumer<Runnable> begin;
-
-    public CooperativeHandler(Runnable task) {
-        this(asyncOf(task));
-    }
-
-    public CooperativeHandler(Consumer<Runnable> begin) {
-        this.begin = requireNonNull(begin);
-    }
-
-    /*
-     * Runs the task (though maybe by a different party).
-     *
-     * The recursion which is possible here will have the maximum depth of 1:
-     *
-     *     this.handle()
-     *         begin.accept()
-     *             this.handle()
-     */
-    public void handle() {
-        while (true) {
-            int s = state.get();
-            if (s == END) {
-                if (state.compareAndSet(END, BEGIN)) {
-                    break;
-                }
-            } else if ((s & BEGIN) != 0) {
-                // Tries to change the state to AGAIN, preserving OFFLOAD bit
-                if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
-                    return;
-                }
-            } else if ((s & AGAIN) != 0 || s == STOP) {
-                return;
-            } else {
-                throw new InternalError(String.valueOf(s));
-            }
-        }
-        begin.accept(this::tryEnd);
-    }
-
-    private void tryEnd() {
-        while (true) {
-            int s;
-            while (((s = state.get()) & OFFLOAD) != 0) {
-                // Tries to offload ending of the task to the parent
-                if (state.compareAndSet(s, s & ~OFFLOAD)) {
-                    return;
-                }
-            }
-            while (true) {
-                if (s == BEGIN) {
-                    if (state.compareAndSet(BEGIN, END)) {
-                        return;
-                    }
-                } else if (s == AGAIN) {
-                    if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
-                        break;
-                    }
-                } else if (s == STOP) {
-                    return;
-                } else {
-                    throw new InternalError(String.valueOf(s));
-                }
-                s = state.get();
-            }
-            begin.accept(this::tryEnd);
-        }
-    }
-
-    /*
-     * Checks whether or not this handler has been permanently stopped.
-     *
-     * Should be used from inside the task to poll the status of the handler,
-     * pretty much the same way as it is done for threads:
-     *
-     *     if (!Thread.currentThread().isInterrupted()) {
-     *         ...
-     *     }
-     */
-    public boolean isStopped() {
-        return state.get() == STOP;
-    }
-
-    /*
-     * Signals this handler to ignore subsequent invocations to `handle()`.
-     *
-     * If the task has already begun, this invocation will not affect it,
-     * unless the task itself uses `isStopped()` method to check the state
-     * of the handler.
-     */
-    public void stop() {
-        state.set(STOP);
-    }
-
-    private static Consumer<Runnable> asyncOf(Runnable task) {
-        requireNonNull(task);
-        return ender -> {
-            task.run();
-            ender.run();
-        };
-    }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/Receiver.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/Receiver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -29,6 +29,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.SelectionKey;
 import java.util.concurrent.atomic.AtomicLong;
+import jdk.incubator.http.internal.common.SequentialScheduler;
 
 /*
  * Receives incoming data from the channel on demand and converts it into a
@@ -57,7 +58,7 @@
     private final Frame.Reader reader = new Frame.Reader();
     private final RawChannel.RawEvent event = createHandler();
     private final AtomicLong demand = new AtomicLong();
-    private final CooperativeHandler handler;
+    private final SequentialScheduler pushScheduler;
 
     private ByteBuffer data;
     private volatile int state;
@@ -74,7 +75,7 @@
         // To ensure the initial non-final `data` will be visible
         // (happens-before) when `handler` invokes `pushContinuously`
         // the following assignment is done last:
-        handler = new CooperativeHandler(this::pushContinuously);
+        pushScheduler = new SequentialScheduler(new PushContinuouslyTask());
     }
 
     private RawChannel.RawEvent createHandler() {
@@ -88,7 +89,7 @@
             @Override
             public void handle() {
                 state = AVAILABLE;
-                handler.handle();
+                pushScheduler.runOrSchedule();
             }
         };
     }
@@ -98,7 +99,7 @@
             throw new IllegalArgumentException("Negative: " + n);
         }
         demand.accumulateAndGet(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
-        handler.handle();
+        pushScheduler.runOrSchedule();
     }
 
     void acknowledge() {
@@ -113,60 +114,64 @@
      * regardless of the current demand and data availability.
      */
     void close() {
-        handler.stop();
+        pushScheduler.stop();
     }
 
-    private void pushContinuously() {
-        while (!handler.isStopped()) {
-            if (data.hasRemaining()) {
-                if (demand.get() > 0) {
-                    try {
-                        int oldPos = data.position();
-                        reader.readFrame(data, frameConsumer);
-                        int newPos = data.position();
-                        assert oldPos != newPos : data; // reader always consumes bytes
-                    } catch (FailWebSocketException e) {
-                        handler.stop();
-                        messageConsumer.onError(e);
+    private class PushContinuouslyTask
+        extends SequentialScheduler.CompleteRestartableTask
+    {
+        @Override
+        public void run() {
+            while (!pushScheduler.isStopped()) {
+                if (data.hasRemaining()) {
+                    if (demand.get() > 0) {
+                        try {
+                            int oldPos = data.position();
+                            reader.readFrame(data, frameConsumer);
+                            int newPos = data.position();
+                            assert oldPos != newPos : data; // reader always consumes bytes
+                        } catch (FailWebSocketException e) {
+                            pushScheduler.stop();
+                            messageConsumer.onError(e);
+                        }
+                        continue;
                     }
-                    continue;
+                    break;
                 }
-                break;
-            }
-            switch (state) {
-                case WAITING:
-                    return;
-                case UNREGISTERED:
-                    try {
-                        state = WAITING;
-                        channel.registerEvent(event);
-                    } catch (IOException e) {
-                        handler.stop();
-                        messageConsumer.onError(e);
-                    }
-                    return;
-                case AVAILABLE:
-                    try {
-                        data = channel.read();
-                    } catch (IOException e) {
-                        handler.stop();
-                        messageConsumer.onError(e);
+                switch (state) {
+                    case WAITING:
+                        return;
+                    case UNREGISTERED:
+                        try {
+                            state = WAITING;
+                            channel.registerEvent(event);
+                        } catch (IOException e) {
+                            pushScheduler.stop();
+                            messageConsumer.onError(e);
+                        }
                         return;
-                    }
-                    if (data == null) { // EOF
-                        handler.stop();
-                        messageConsumer.onComplete();
-                        return;
-                    } else if (!data.hasRemaining()) { // No data at the moment
-                        // Pretty much a "goto", reusing the existing code path
-                        // for registration
-                        state = UNREGISTERED;
-                    }
-                    continue;
-                default:
-                    throw new InternalError(String.valueOf(state));
+                    case AVAILABLE:
+                        try {
+                            data = channel.read();
+                        } catch (IOException e) {
+                            pushScheduler.stop();
+                            messageConsumer.onError(e);
+                            return;
+                        }
+                        if (data == null) { // EOF
+                            pushScheduler.stop();
+                            messageConsumer.onComplete();
+                            return;
+                        } else if (!data.hasRemaining()) { // No data at the moment
+                            // Pretty much a "goto", reusing the existing code path
+                            // for registration
+                            state = UNREGISTERED;
+                        }
+                        continue;
+                    default:
+                        throw new InternalError(String.valueOf(state));
+                }
             }
         }
     }
 }
-
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,17 +25,6 @@
 
 package jdk.incubator.http.internal.websocket;
 
-import jdk.incubator.http.WebSocket;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Pair;
-import jdk.incubator.http.internal.websocket.OpeningHandshake.Result;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Binary;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Close;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Context;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Ping;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Pong;
-import jdk.incubator.http.internal.websocket.OutgoingMessage.Text;
-
 import java.io.IOException;
 import java.net.ProtocolException;
 import java.net.URI;
@@ -47,10 +36,23 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import jdk.incubator.http.WebSocket;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Pair;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.websocket.OpeningHandshake.Result;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Binary;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Close;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Context;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Ping;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Pong;
+import jdk.incubator.http.internal.websocket.OutgoingMessage.Text;
 
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.CompletableFuture.failedFuture;
 import static jdk.incubator.http.internal.common.Pair.pair;
+import jdk.incubator.http.internal.common.Utils;
 import static jdk.incubator.http.internal.websocket.StatusCodes.CLOSED_ABNORMALLY;
 import static jdk.incubator.http.internal.websocket.StatusCodes.NO_STATUS_CODE;
 import static jdk.incubator.http.internal.websocket.StatusCodes.isLegalToSendFromClient;
@@ -72,8 +74,7 @@
      */
     private boolean lastMethodInvoked;
     private final AtomicBoolean outstandingSend = new AtomicBoolean();
-    private final CooperativeHandler sendHandler =
-              new CooperativeHandler(this::sendFirst);
+    private final SequentialScheduler sendScheduler;
     private final Queue<Pair<OutgoingMessage, CompletableFuture<WebSocket>>>
             queue = new ConcurrentLinkedQueue<>();
     private final Context context = new OutgoingMessage.Context();
@@ -136,6 +137,7 @@
         this.listener = requireNonNull(listener);
         this.transmitter = new Transmitter(channel);
         this.receiver = new Receiver(messageConsumerOf(listener), channel);
+        this.sendScheduler = new SequentialScheduler(new SendFirstTask());
 
         // Set up the Closing Handshake action
         CompletableFuture.allOf(closeReceived, closeSent)
@@ -325,36 +327,39 @@
             // The queue is supposed to be unbounded
             throw new InternalError();
         }
-        sendHandler.handle();
+        sendScheduler.runOrSchedule();
         return cf;
     }
 
     /*
-     * This is the main sending method. It may be run in different threads,
+     * This is the main sending task. It may be run in different threads,
      * but never concurrently.
      */
-    private void sendFirst(Runnable whenSent) {
-        Pair<OutgoingMessage, CompletableFuture<WebSocket>> p = queue.poll();
-        if (p == null) {
-            whenSent.run();
-            return;
-        }
-        OutgoingMessage message = p.first;
-        CompletableFuture<WebSocket> cf = p.second;
-        try {
-            message.contextualize(context);
-            Consumer<Exception> h = e -> {
-                if (e == null) {
-                    cf.complete(WebSocketImpl.this);
-                } else {
-                    cf.completeExceptionally(e);
-                }
-                sendHandler.handle();
-                whenSent.run();
-            };
-            transmitter.send(message, h);
-        } catch (Exception t) {
-            cf.completeExceptionally(t);
+    private class SendFirstTask implements SequentialScheduler.RestartableTask {
+        @Override
+        public void run (DeferredCompleter taskCompleter){
+            Pair<OutgoingMessage, CompletableFuture<WebSocket>> p = queue.poll();
+            if (p == null) {
+                taskCompleter.complete();
+                return;
+            }
+            OutgoingMessage message = p.first;
+            CompletableFuture<WebSocket> cf = p.second;
+            try {
+                message.contextualize(context);
+                Consumer<Exception> h = e -> {
+                    if (e == null) {
+                        cf.complete(WebSocketImpl.this);
+                    } else {
+                        cf.completeExceptionally(e);
+                    }
+                    sendScheduler.runOrSchedule();
+                    taskCompleter.complete();
+                };
+                transmitter.send(message, h);
+            } catch (Exception t) {
+                cf.completeExceptionally(t);
+            }
         }
     }
 
@@ -436,7 +441,8 @@
                 pongSent.whenComplete(
                         (r, error) -> {
                             if (error != null) {
-                                WebSocketImpl.this.signalError(error);
+                                WebSocketImpl.this.signalError(
+                                        Utils.getCompletionCause(error));
                             }
                         }
                 );
@@ -482,7 +488,7 @@
                     enqueueClose(new Close(code, ""))
                             .whenComplete((r, e) -> {
                                 if (e != null) {
-                                    ex.addSuppressed(e);
+                                    ex.addSuppressed(Utils.getCompletionCause(e));
                                 }
                                 try {
                                     channel.close();
--- a/src/jdk.incubator.httpclient/share/classes/module-info.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/module-info.java	Sun Nov 05 17:32:13 2017 +0000
@@ -33,4 +33,3 @@
 module jdk.incubator.httpclient {
     exports jdk.incubator.http;
 }
-
--- a/test/jdk/java/net/httpclient/APIErrors.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,193 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, 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
- * @bug 8087112
- * @modules jdk.incubator.httpclient
- *          java.logging
- *          jdk.httpserver
- * @library /lib/testlibrary/
- * @build jdk.testlibrary.SimpleSSLContext ProxyServer
- * @build TestKit
- * @compile ../../../com/sun/net/httpserver/LogFilter.java
- * @compile ../../../com/sun/net/httpserver/FileServerHandler.java
- * @run main/othervm APIErrors
- * @summary  Basic checks for appropriate errors/exceptions thrown from the API
- */
-
-import com.sun.net.httpserver.HttpContext;
-import com.sun.net.httpserver.HttpHandler;
-import com.sun.net.httpserver.HttpServer;
-import com.sun.net.httpserver.HttpsServer;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.PasswordAuthentication;
-import java.net.ProxySelector;
-import java.net.URI;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Supplier;
-import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
-
-public class APIErrors {
-
-    static ExecutorService serverExecutor = Executors.newCachedThreadPool();
-    static String httproot, fileuri;
-    static List<HttpClient> clients = new LinkedList<>();
-
-    public static void main(String[] args) throws Exception {
-        HttpServer server = createServer();
-
-        int port = server.getAddress().getPort();
-        System.out.println("HTTP server port = " + port);
-
-        httproot = "http://127.0.0.1:" + port + "/files/";
-        fileuri = httproot + "foo.txt";
-
-        HttpClient client = HttpClient.newHttpClient();
-
-        try {
-            test1();
-            test2();
-            //test3();
-        } finally {
-            server.stop(0);
-            serverExecutor.shutdownNow();
-            for (HttpClient c : clients)
-                ((ExecutorService)c.executor()).shutdownNow();
-        }
-    }
-
-    static void checkNonNull(Supplier<?> r) {
-        if (r.get() == null)
-            throw new RuntimeException("Unexpected null return:");
-    }
-
-    static void assertTrue(Supplier<Boolean> r) {
-        if (r.get() == false)
-            throw new RuntimeException("Assertion failure:");
-    }
-
-    // HttpClient.Builder
-    static void test1() throws Exception {
-        System.out.println("Test 1");
-        HttpClient.Builder cb = HttpClient.newBuilder();
-        TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(-1));
-        TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(0));
-        TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(257));
-        TestKit.assertThrows(IllegalArgumentException.class, () -> cb.priority(500));
-        TestKit.assertNotThrows(() -> cb.priority(1));
-        TestKit.assertNotThrows(() -> cb.priority(256));
-        TestKit.assertNotThrows(() -> {
-            clients.add(cb.build());
-            clients.add(cb.build());
-        });
-    }
-
-    static void test2() throws Exception {
-        System.out.println("Test 2");
-        HttpClient.Builder cb = HttpClient.newBuilder();
-        InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 5000);
-        cb.proxy(ProxySelector.of(addr));
-        HttpClient c = cb.build();
-        clients.add(c);
-        checkNonNull(()-> c.executor());
-        assertTrue(()-> c.followRedirects() == HttpClient.Redirect.NEVER);
-        assertTrue(()-> !c.authenticator().isPresent());
-    }
-
-    static URI accessibleURI() {
-        return URI.create(fileuri);
-    }
-
-    static HttpRequest request() {
-        return HttpRequest.newBuilder(accessibleURI()).GET().build();
-    }
-
-//    static void test3() throws Exception {
-//        System.out.println("Test 3");
-//        TestKit.assertThrows(IllegalStateException.class, ()-> {
-//            try {
-//                HttpRequest r1 = request();
-//                HttpResponse<Object> resp = r1.response(discard(null));
-//                HttpResponse<Object> resp1 = r1.response(discard(null));
-//            } catch (IOException |InterruptedException e) {
-//                throw new RuntimeException(e);
-//            }
-//        });
-//
-//        TestKit.assertThrows(IllegalStateException.class, ()-> {
-//            try {
-//                HttpRequest r1 = request();
-//                HttpResponse<Object> resp = r1.response(discard(null));
-//                HttpResponse<Object> resp1 = r1.responseAsync(discard(null)).get();
-//            } catch (IOException |InterruptedException | ExecutionException e) {
-//                throw new RuntimeException(e);
-//            }
-//        });
-//        TestKit.assertThrows(IllegalStateException.class, ()-> {
-//            try {
-//                HttpRequest r1 = request();
-//                HttpResponse<Object> resp1 = r1.responseAsync(discard(null)).get();
-//                HttpResponse<Object> resp = r1.response(discard(null));
-//            } catch (IOException |InterruptedException | ExecutionException e) {
-//                throw new RuntimeException(e);
-//            }
-//        });
-//    }
-
-    static class Auth extends java.net.Authenticator {
-        int count = 0;
-        @Override
-        protected PasswordAuthentication getPasswordAuthentication() {
-            if (count++ == 0) {
-                return new PasswordAuthentication("user", "passwd".toCharArray());
-            } else {
-                return new PasswordAuthentication("user", "goober".toCharArray());
-            }
-        }
-        int count() {
-            return count;
-        }
-    }
-
-    static HttpServer createServer() throws Exception {
-        HttpServer s = HttpServer.create(new InetSocketAddress(0), 0);
-        if (s instanceof HttpsServer)
-            throw new RuntimeException ("should not be httpsserver");
-
-        String root = System.getProperty("test.src") + "/docs";
-        s.createContext("/files", new FileServerHandler(root));
-        s.setExecutor(serverExecutor);
-        s.start();
-
-        return s;
-    }
-}
--- a/test/jdk/java/net/httpclient/BasicAuthTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/BasicAuthTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -45,7 +45,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import static java.nio.charset.StandardCharsets.US_ASCII;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
 public class BasicAuthTest {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2017, 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.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.stream.Stream;
+import static java.lang.System.err;
+
+/*
+ * @test
+ * @bug 8187503
+ * @summary An example on how to read a response body with InputStream...
+ * @run main/othervm -Dtest.debug=true BodyProcessorInputStreamTest
+ * @author daniel fuchs
+ */
+public class BodyProcessorInputStreamTest {
+
+    public static boolean DEBUG = Boolean.getBoolean("test.debug");
+
+    /**
+     * Examine the response headers to figure out the charset used to
+     * encode the body content.
+     * If the content type is not textual, returns an empty Optional.
+     * Otherwise, returns the body content's charset, defaulting to
+     * ISO-8859-1 if none is explicitly specified.
+     * @param headers The response headers.
+     * @return The charset to use for decoding the response body, if
+     *         the response body content is text/...
+     */
+    public static Optional<Charset> getCharset(HttpHeaders headers) {
+        Optional<String> contentType = headers.firstValue("Content-Type");
+        Optional<Charset> charset = Optional.empty();
+        if (contentType.isPresent()) {
+            final String[] values = contentType.get().split(";");
+            if (values[0].startsWith("text/")) {
+                charset = Optional.of(Stream.of(values)
+                    .map(x -> x.toLowerCase(Locale.ROOT))
+                    .map(String::trim)
+                    .filter(x -> x.startsWith("charset="))
+                    .map(x -> x.substring("charset=".length()))
+                    .findFirst()
+                    .orElse("ISO-8859-1"))
+                    .map(Charset::forName);
+            }
+        }
+        return charset;
+    }
+
+    public static void main(String[] args) throws Exception {
+        HttpClient client = HttpClient.newHttpClient();
+        HttpRequest request = HttpRequest
+            .newBuilder(new URI("http://hg.openjdk.java.net/jdk9/sandbox/jdk/shortlog/http-client-branch/"))
+            .GET()
+            .build();
+
+        // This example shows how to return an InputStream that can be used to
+        // start reading the response body before the response is fully received.
+        // In comparison, the snipet below (which uses
+        // HttpResponse.BodyHandler.asString()) obviously will not return before the
+        // response body is fully read:
+        //
+        // System.out.println(
+        //    client.sendAsync(request, HttpResponse.BodyHandler.asString()).get().body());
+
+        CompletableFuture<HttpResponse<InputStream>> handle =
+            client.sendAsync(request, HttpResponse.BodyHandler.asInputStream());
+        if (DEBUG) err.println("Request sent");
+
+        HttpResponse<InputStream> pending = handle.get();
+
+        // At this point, the response headers have been received, but the
+        // response body may not have arrived yet. This comes from
+        // the implementation of HttpResponseInputStream::getBody above,
+        // which returns an already completed completion stage, without
+        // waiting for any data.
+        // We can therefore access the headers - and the body, which
+        // is our live InputStream, without waiting...
+        HttpHeaders responseHeaders = pending.headers();
+
+        // Get the charset declared in the response headers.
+        // The optional will be empty if the content type is not
+        // of type text/...
+        Optional<Charset> charset = getCharset(responseHeaders);
+
+        try (InputStream is = pending.body();
+            // We assume a textual content type. Construct an InputStream
+            // Reader with the appropriate Charset.
+            // charset.get() will throw NPE if the content is not textual.
+            Reader r = new InputStreamReader(is, charset.get())) {
+
+            char[] buff = new char[32];
+            int off=0, n=0;
+            if (DEBUG) err.println("Start receiving response body");
+            if (DEBUG) err.println("Charset: " + charset.get());
+
+            // Start consuming the InputStream as the data arrives.
+            // Will block until there is something to read...
+            while ((n = r.read(buff, off, buff.length - off)) > 0) {
+                assert (buff.length - off) > 0;
+                assert n <= (buff.length - off);
+                if (n == (buff.length - off)) {
+                    System.out.print(buff);
+                    off = 0;
+                } else {
+                    off += n;
+                }
+                assert off < buff.length;
+            }
+
+            // last call to read may not have filled 'buff' completely.
+            // flush out the remaining characters.
+            assert off >= 0 && off < buff.length;
+            for (int i=0; i < off; i++) {
+                System.out.print(buff[i]);
+            }
+
+            // We're done!
+            System.out.println("Done!");
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/BufferingSubscriberTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2017, 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.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.SubmissionPublisher;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
+import jdk.test.lib.RandomFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.Long.MAX_VALUE;
+import static java.lang.System.out;
+import static java.util.concurrent.CompletableFuture.delayedExecutor;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.testng.Assert.*;
+
+/*
+ * @test
+ * @bug 8184285
+ * @summary Direct test for HttpResponse.BodySubscriber.buffering() API
+ * @key randomness
+ * @library /test/lib
+ * @build jdk.test.lib.RandomFactory
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true BufferingSubscriberTest
+ */
+
+public class BufferingSubscriberTest {
+
+    static final Random random = RandomFactory.getRandom();
+
+    @DataProvider(name = "negatives")
+    public Object[][] negatives() {
+        return new Object[][] {  { 0 }, { -1 }, { -1000 } };
+    }
+
+    @Test(dataProvider = "negatives", expectedExceptions = IllegalArgumentException.class)
+    public void subscriberThrowsIAE(int bufferSize) {
+        BodySubscriber<?> bp = BodySubscriber.asByteArray();
+        BodySubscriber.buffering(bp, bufferSize);
+    }
+
+    @Test(dataProvider = "negatives", expectedExceptions = IllegalArgumentException.class)
+    public void handlerThrowsIAE(int bufferSize) {
+        BodyHandler<?> bp = BodyHandler.asByteArray();
+        BodyHandler.buffering(bp, bufferSize);
+    }
+
+    // ---
+
+    @DataProvider(name = "config")
+    public Object[][] config() {
+        return new Object[][] {
+            // iterations delayMillis numBuffers bufferSize maxBufferSize minBufferSize
+            { 1,              0,          1,         1,         2,            1   },
+            { 1,              0,          1,         10,        1000,         1   },
+            { 1,              10,         1,         10,        1000,         1   },
+            { 1,              0,          1,         1000,      1000,         1   },
+            { 1,              0,          10,        1000,      1000,         1   },
+            { 1,              0,          1000,      10 ,       1000,         50  },
+            { 1,              100,        1,         1000 * 4,  1000,         1   },
+            { 100,            0,          1000,      1,         2,            1   },
+            { 3,              0,          4,         5006,      1000,         1   },
+            { 20,             0,          100,       4888,      1000,         100 },
+            { 16,             10,         1000,      50 ,       1000,         100 },
+        };
+    }
+
+    @Test(dataProvider = "config")
+    public void test(int iterations,
+                     int delayMillis,
+                     int numBuffers,
+                     int bufferSize,
+                     int maxBufferSize,
+                     int minbufferSize) {
+        for (long perRequestAmount : new long[] { 1L, MAX_VALUE })
+            test(iterations,
+                 delayMillis,
+                 numBuffers,
+                 bufferSize,
+                 maxBufferSize,
+                 minbufferSize,
+                 perRequestAmount);
+    }
+
+    public void test(int iterations,
+                     int delayMillis,
+                     int numBuffers,
+                     int bufferSize,
+                     int maxBufferSize,
+                     int minBufferSize,
+                     long requestAmount) {
+        ExecutorService executor = Executors.newFixedThreadPool(1);
+        try {
+
+            out.printf("Iterations %d\n", iterations);
+            for (int i=0; i<iterations; i++ ) {
+                out.printf("Iteration: %d\n", i);
+                SubmissionPublisher<List<ByteBuffer>> publisher =
+                        new SubmissionPublisher<>(executor, 1);
+                CompletableFuture<?> cf = sink(publisher,
+                        delayMillis,
+                        numBuffers * bufferSize,
+                        requestAmount,
+                        maxBufferSize,
+                        minBufferSize);
+                source(publisher, numBuffers, bufferSize);
+                publisher.close();
+                cf.join();
+            }
+            out.println("OK");
+        } finally {
+            executor.shutdown();
+        }
+    }
+
+    static int accumulatedDataSize(List<ByteBuffer> bufs) {
+        return bufs.stream().mapToInt(ByteBuffer::remaining).sum();
+    }
+
+    /** Returns a new BB with its contents set to monotonically increasing
+     * values, staring at the given start index and wrapping every 100. */
+    static ByteBuffer allocateBuffer(int size, int startIdx) {
+        ByteBuffer b = ByteBuffer.allocate(size);
+        for (int i=0; i<size; i++)
+            b.put((byte)((startIdx + i) % 100));
+        b.position(0);
+        return b;
+    }
+
+    static class TestSubscriber implements BodySubscriber<Integer> {
+        final int delayMillis;
+        final int bufferSize;
+        final int expectedTotalSize;
+        final long requestAmount;
+        final CompletableFuture<Integer> completion;
+        final Executor delayedExecutor;
+        volatile Flow.Subscription subscription;
+
+        TestSubscriber(int bufferSize,
+                       int delayMillis,
+                       int expectedTotalSize,
+                       long requestAmount) {
+            this.bufferSize = bufferSize;
+            this.completion = new CompletableFuture<>();
+            this.delayMillis = delayMillis;
+            this.delayedExecutor = delayedExecutor(delayMillis, MILLISECONDS);
+            this.expectedTotalSize = expectedTotalSize;
+            this.requestAmount = requestAmount;
+        }
+
+        /**
+         * Example of a factory method which would decorate a buffering
+         * subscriber to create a new subscriber dependent on buffering capability.
+         *
+         * The integer type parameter simulates the body just by counting the
+         * number of bytes in the body.
+         */
+        static BodySubscriber<Integer> createSubscriber(int bufferSize,
+                                                        int delay,
+                                                        int expectedTotalSize,
+                                                        long requestAmount) {
+            TestSubscriber s = new TestSubscriber(bufferSize,
+                                                delay,
+                                                expectedTotalSize,
+                                                requestAmount);
+            return BodySubscriber.buffering(s, bufferSize);
+        }
+
+        private void requestMore() { subscription.request(requestAmount); }
+
+        @Override
+        public void onSubscribe(Subscription subscription) {
+            assertNull(this.subscription);
+            this.subscription = subscription;
+            if (delayMillis > 0)
+                delayedExecutor.execute(this::requestMore);
+            else
+                requestMore();
+        }
+
+        volatile int wrongSizes;
+        volatile int totalBytesReceived;
+        volatile int onNextInvocations;
+        volatile int lastSeenSize = -1;
+        volatile boolean noMoreOnNext; // false
+        volatile int index; // 0
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            long sz = accumulatedDataSize(items);
+            onNextInvocations++;
+            assertNotEquals(sz, 0L, "Unexpected empty buffers");
+            items.stream().forEach(b -> assertEquals(b.position(), 0));
+            assertFalse(noMoreOnNext);
+
+            if (sz != bufferSize) {
+                String msg = sz + ", should be less than bufferSize, " + bufferSize;
+                assertTrue(sz < bufferSize, msg);
+                assertTrue(lastSeenSize == -1 || lastSeenSize == bufferSize);
+                noMoreOnNext = true;
+                wrongSizes++;
+            } else {
+                assertEquals(sz, bufferSize, "Expected to receive exactly bufferSize");
+            }
+
+            // Ensure expected contents
+            for (ByteBuffer b : items) {
+                while (b.hasRemaining()) {
+                    assertEquals(b.get(), (byte) (index % 100));
+                    index++;
+                }
+            }
+
+            totalBytesReceived += sz;
+            assertEquals(totalBytesReceived, index );
+            if (delayMillis > 0)
+                delayedExecutor.execute(this::requestMore);
+            else
+                requestMore();
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            completion.completeExceptionally(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            if (wrongSizes > 1) { // allow just the final item to be smaller
+                String msg = "Wrong sizes. Expected no more than 1. [" + this + "]";
+                completion.completeExceptionally(new Throwable(msg));
+            }
+            if (totalBytesReceived != expectedTotalSize) {
+                String msg = "Wrong number of bytes. [" + this + "]";
+                completion.completeExceptionally(new Throwable(msg));
+            } else {
+                completion.complete(totalBytesReceived);
+            }
+        }
+
+        @Override
+        public CompletionStage<Integer> getBody() { return completion; }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            sb.append(", bufferSize=").append(bufferSize);
+            sb.append(", onNextInvocations=").append(onNextInvocations);
+            sb.append(", totalBytesReceived=").append(totalBytesReceived);
+            sb.append(", expectedTotalSize=").append(expectedTotalSize);
+            sb.append(", requestAmount=").append(requestAmount);
+            sb.append(", lastSeenSize=").append(lastSeenSize);
+            sb.append(", wrongSizes=").append(wrongSizes);
+            sb.append(", index=").append(index);
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Publishes data, through the given publisher, using the main thread.
+     *
+     * Note: The executor supplied when creating the SubmissionPublisher provides
+     * the threads for executing the Subscribers.
+     *
+     * @param publisher the publisher
+     * @param numBuffers the number of buffers to send ( before splitting in two )
+     * @param bufferSize the total size of the data to send ( before splitting in two )
+     */
+    static void source(SubmissionPublisher<List<ByteBuffer>> publisher,
+                       int numBuffers,
+                       int bufferSize) {
+        out.printf("Publishing %d buffers of size %d each\n", numBuffers, bufferSize);
+        int index = 0;
+        for (int i=0; i<numBuffers; i++) {
+            int chunkSize = random.nextInt(bufferSize);
+            ByteBuffer buf1 = allocateBuffer(chunkSize, index);
+            index += chunkSize;
+            ByteBuffer buf2 = allocateBuffer(bufferSize - chunkSize, index);
+            index += bufferSize - chunkSize;
+            publisher.submit(List.of(buf1, buf2));
+        }
+        out.println("source complete");
+    }
+
+    /**
+     * Creates and subscribes Subscribers that receive data from the given
+     * publisher.
+     *
+     * @param publisher the publisher
+     * @param delayMillis time, in milliseconds, to delay the Subscription
+     *                    requesting more bytes ( for simulating slow consumption )
+     * @param expectedTotalSize the total number of bytes expected to be received
+     *                          by the subscribers
+     * @return a CompletableFuture which completes when the subscription is complete
+     */
+    static CompletableFuture<?> sink(SubmissionPublisher<List<ByteBuffer>> publisher,
+                                     int delayMillis,
+                                     int expectedTotalSize,
+                                     long requestAmount,
+                                     int maxBufferSize,
+                                     int minBufferSize) {
+        int bufferSize = random.nextInt(maxBufferSize - minBufferSize) + minBufferSize;
+        BodySubscriber<Integer> sub = TestSubscriber.createSubscriber(bufferSize,
+                                                                    delayMillis,
+                                                                    expectedTotalSize,
+                                                                    requestAmount);
+        publisher.subscribe(sub);
+        out.printf("Subscriber reads data with buffer size: %d\n", bufferSize);
+        out.printf("Subscription delay is %d msec\n", delayMillis);
+        out.printf("Request amount is %d items\n", requestAmount);
+        return sub.getBody().toCompletableFuture();
+    }
+
+    // ---
+
+    // TODO: Add a test for cancel
+
+    // ---
+
+    /* Main entry point for standalone testing of the main functional test. */
+    public static void main(String... args) {
+        BufferingSubscriberTest t = new BufferingSubscriberTest();
+        for (Object[] objs : t.config())
+            t.test((int)objs[0], (int)objs[1], (int)objs[2], (int)objs[3], (int)objs[4], (int)objs[5]);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2017, 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 Checks correct handling of Publishers that call onComplete without demand
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ *          java.logging
+ *          jdk.httpserver
+ * @library /lib/testlibrary http2/server
+ * @build Http2TestServer
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm CustomRequestPublisher
+ */
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+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.nio.charset.StandardCharsets.US_ASCII;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class CustomRequestPublisher {
+
+    SSLContext sslContext;
+    HttpServer httpTestServer;         // HTTP/1.1    [ 4 servers ]
+    HttpsServer httpsTestServer;       // HTTPS/1.1
+    Http2TestServer http2TestServer;   // HTTP/2 ( h2c )
+    Http2TestServer https2TestServer;  // HTTP/2 ( h2  )
+    String httpURI;
+    String httpsURI;
+    String http2URI;
+    String https2URI;
+
+    @DataProvider(name = "variants")
+    public Object[][] variants() {
+        Supplier<BodyPublisher> fixedSupplier   = () -> new FixedLengthBodyPublisher();
+        Supplier<BodyPublisher> unknownSupplier = () -> new UnknownLengthBodyPublisher();
+
+        return new Object[][]{
+                { httpURI,   fixedSupplier,   false },
+                { httpURI,   unknownSupplier, false },
+                { httpsURI,  fixedSupplier,   false },
+                { httpsURI,  unknownSupplier, false },
+                { http2URI,  fixedSupplier,   false },
+                { http2URI,  unknownSupplier, false },
+                { https2URI, fixedSupplier,   false,},
+                { https2URI, unknownSupplier, false },
+
+                { httpURI,   fixedSupplier,   true },
+                { httpURI,   unknownSupplier, true },
+                { httpsURI,  fixedSupplier,   true },
+                { httpsURI,  unknownSupplier, true },
+                { http2URI,  fixedSupplier,   true },
+                { http2URI,  unknownSupplier, true },
+                { https2URI, fixedSupplier,   true,},
+                { https2URI, unknownSupplier, true },
+        };
+    }
+
+    static final int ITERATION_COUNT = 10;
+
+    @Test(dataProvider = "variants")
+    void test(String uri, Supplier<BodyPublisher> bpSupplier, boolean sameClient)
+            throws Exception
+    {
+        HttpClient client = null;
+        for (int i=0; i< ITERATION_COUNT; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+            BodyPublisher bodyPublisher = bpSupplier.get();
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .POST(bodyPublisher)
+                    .build();
+
+            HttpResponse<String> resp = client.send(request, asString());
+
+            out.println("Got response: " + resp);
+            out.println("Got body: " + resp.body());
+            assertTrue(resp.statusCode() == 200,
+                    "Expected 200, got:" + resp.statusCode());
+            assertEquals(resp.body(), bodyPublisher.bodyAsString());
+        }
+    }
+
+    @Test(dataProvider = "variants")
+    void testAsync(String uri, Supplier<BodyPublisher> bpSupplier, boolean sameClient)
+            throws Exception
+    {
+        HttpClient client = null;
+        for (int i=0; i< ITERATION_COUNT; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+            BodyPublisher bodyPublisher = bpSupplier.get();
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .POST(bodyPublisher)
+                    .build();
+
+            CompletableFuture<HttpResponse<String>> cf = client.sendAsync(request, asString());
+            HttpResponse<String> resp = cf.get();
+
+            out.println("Got response: " + resp);
+            out.println("Got body: " + resp.body());
+            assertTrue(resp.statusCode() == 200,
+                    "Expected 200, got:" + resp.statusCode());
+            assertEquals(resp.body(), bodyPublisher.bodyAsString());
+        }
+    }
+
+    /** A Publisher that returns an UNKNOWN content length. */
+    static class UnknownLengthBodyPublisher extends BodyPublisher {
+        @Override
+        public long contentLength() {
+            return -1;  // unknown
+        }
+    }
+
+    /** A Publisher that returns a FIXED content length. */
+    static class FixedLengthBodyPublisher extends BodyPublisher {
+        final int LENGTH = Arrays.stream(BODY)
+                .mapToInt(s-> s.getBytes(US_ASCII).length)
+                .sum();
+        @Override
+        public long contentLength() {
+            return LENGTH;
+        }
+    }
+
+    /**
+     * A Publisher that ( quite correctly ) invokes onComplete, after the last
+     * item has been published, even without any outstanding demand.
+     */
+    static abstract class BodyPublisher implements HttpRequest.BodyPublisher {
+
+        String[] BODY = new String[]
+                { "Say ", "Hello ", "To ", "My ", "Little ", "Friend" };
+
+        protected volatile Flow.Subscriber subscriber;
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+            this.subscriber = subscriber;
+            subscriber.onSubscribe(new InternalSubscription());
+        }
+
+        @Override
+        public abstract long contentLength();
+
+        String bodyAsString() {
+            return Arrays.stream(BODY).collect(Collectors.joining());
+        }
+
+        class InternalSubscription implements Flow.Subscription {
+
+            private final AtomicLong demand = new AtomicLong();
+            private final AtomicBoolean cancelled = new AtomicBoolean();
+            private volatile int position;
+
+            private static final int IDLE    =  1;
+            private static final int PUSHING =  2;
+            private static final int AGAIN   =  4;
+            private final AtomicInteger state = new AtomicInteger(IDLE);
+
+            @Override
+            public void request(long n) {
+                if (n <= 0L) {
+                    subscriber.onError(new IllegalArgumentException(
+                            "non-positive subscription request"));
+                    return;
+                }
+                if (cancelled.get()) {
+                    return;
+                }
+
+                while (true) {
+                    long prev = demand.get(), d;
+                    if ((d = prev + n) < prev) // saturate
+                        d = Long.MAX_VALUE;
+                    if (demand.compareAndSet(prev, d))
+                        break;
+                }
+
+                while (true) {
+                    int s = state.get();
+                    if (s == IDLE) {
+                        if (state.compareAndSet(IDLE, PUSHING)) {
+                            while (true) {
+                                push();
+                                if (state.compareAndSet(PUSHING, IDLE))
+                                    return;
+                                else if (state.compareAndSet(AGAIN, PUSHING))
+                                    continue;
+                            }
+                        }
+                    } else if (s == PUSHING) {
+                        if (state.compareAndSet(PUSHING, AGAIN))
+                            return;
+                    } else if (s == AGAIN){
+                        // do nothing, the pusher will already rerun
+                        return;
+                    } else {
+                        throw new AssertionError("Unknown state:" + s);
+                    }
+                }
+            }
+
+            private void push() {
+                long prev;
+                while ((prev = demand.get()) > 0) {
+                    if (!demand.compareAndSet(prev, prev -1))
+                        continue;
+
+                    int index = position;
+                    if (index < BODY.length) {
+                        position++;
+                        subscriber.onNext(ByteBuffer.wrap(BODY[index].getBytes(US_ASCII)));
+                    }
+                }
+
+                if (position == BODY.length && !cancelled.get()) {
+                    cancelled.set(true);
+                    subscriber.onComplete();  // NOTE: onComplete without demand
+                }
+            }
+
+            @Override
+            public void cancel() {
+                if (cancelled.compareAndExchange(false, true))
+                    return;  // already cancelled
+            }
+        }
+    }
+
+    @BeforeTest
+    public void setup() throws Exception {
+        sslContext = new SimpleSSLContext().get();
+        if (sslContext == null)
+            throw new AssertionError("Unexpected null sslContext");
+
+        InetSocketAddress sa = new InetSocketAddress("localhost", 0);
+        httpTestServer = HttpServer.create(sa, 0);
+        httpTestServer.createContext("/http1/echo", new Http1EchoHandler());
+        httpURI = "http://127.0.0.1:" + httpTestServer.getAddress().getPort() + "/http1/echo";
+
+        httpsTestServer = HttpsServer.create(sa, 0);
+        httpsTestServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+        httpsTestServer.createContext("/https1/echo", new Http1EchoHandler());
+        httpsURI = "https://127.0.0.1:" + httpsTestServer.getAddress().getPort() + "/https1/echo";
+
+        http2TestServer = new Http2TestServer("127.0.0.1", false, 0);
+        http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
+        int port = http2TestServer.getAddress().getPort();
+        http2URI = "http://127.0.0.1:" + port + "/http2/echo";
+
+        https2TestServer = new Http2TestServer("127.0.0.1", true, 0);
+        https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
+        port = https2TestServer.getAddress().getPort();
+        https2URI = "https://127.0.0.1:" + port + "/https2/echo";
+
+        httpTestServer.start();
+        httpsTestServer.start();
+        http2TestServer.start();
+        https2TestServer.start();
+    }
+
+    @AfterTest
+    public void teardown() throws Exception {
+        httpTestServer.stop(0);
+        httpsTestServer.stop(0);
+        http2TestServer.stop();
+        https2TestServer.stop();
+    }
+
+    static class Http1EchoHandler implements HttpHandler {
+        @Override
+        public void handle(HttpExchange t) throws IOException {
+            try (InputStream is = t.getRequestBody();
+                 OutputStream os = t.getResponseBody()) {
+                byte[] bytes = is.readAllBytes();
+                t.sendResponseHeaders(200, bytes.length);
+                os.write(bytes);
+            }
+        }
+    }
+
+    static class Http2EchoHandler implements Http2Handler {
+        @Override
+        public void handle(Http2TestExchange t) throws IOException {
+            try (InputStream is = t.getRequestBody();
+                 OutputStream os = t.getResponseBody()) {
+                byte[] bytes = is.readAllBytes();
+                t.sendResponseHeaders(200, bytes.length);
+                os.write(bytes);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2017, 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 javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocket;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpRequest;
+import static java.lang.System.out;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
+
+/**
+ * @test
+ * @run main/othervm HandshakeFailureTest
+ * @summary Verify SSLHandshakeException is received when the handshake fails,
+ * either because the server closes ( EOF ) the connection during handshaking
+ * or no cipher suite ( or similar ) can be negotiated.
+ */
+// To switch on debugging use:
+// @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest
+public class HandshakeFailureTest {
+
+    // The number of iterations each testXXXClient performs. Can be increased
+    // when running standalone testing.
+    static final int TIMES = 10;
+
+    public static void main(String[] args) throws Exception {
+        HandshakeFailureTest test = new HandshakeFailureTest();
+        List<AbstractServer> servers = List.of( new PlainServer(), new SSLServer());
+
+        for (AbstractServer server : servers) {
+            try (server) {
+                out.format("%n%n------ Testing with server:%s ------%n", server);
+                URI uri = new URI("https://127.0.0.1:" + server.getPort() + "/");
+
+                test.testSyncSameClient(uri, Version.HTTP_1_1);
+                test.testSyncSameClient(uri, Version.HTTP_2);
+                test.testSyncDiffClient(uri, Version.HTTP_1_1);
+                test.testSyncDiffClient(uri, Version.HTTP_2);
+
+                test.testAsyncSameClient(uri, Version.HTTP_1_1);
+                test.testAsyncSameClient(uri, Version.HTTP_2);
+                test.testAsyncDiffClient(uri, Version.HTTP_1_1);
+                test.testAsyncDiffClient(uri, Version.HTTP_2);
+            }
+        }
+    }
+
+    void testSyncSameClient(URI uri, Version version) throws Exception {
+        out.printf("%n--- testSyncSameClient %s ---%n", version);
+        HttpClient client = HttpClient.newHttpClient();
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            HttpRequest request = HttpRequest.newBuilder(uri)
+                                             .version(version)
+                                             .build();
+            try {
+                HttpResponse<Void> response = client.send(request, discard(null));
+                String msg = String.format("UNEXPECTED response=%s%n", response);
+                throw new RuntimeException(msg);
+            } catch (SSLHandshakeException expected) {
+                out.printf("Client: caught expected exception: %s%n", expected);
+            }
+        }
+    }
+
+    void testSyncDiffClient(URI uri, Version version) throws Exception {
+        out.printf("%n--- testSyncDiffClient %s ---%n", version);
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            // a new client each time
+            HttpClient client = HttpClient.newHttpClient();
+            HttpRequest request = HttpRequest.newBuilder(uri)
+                                             .version(version)
+                                             .build();
+            try {
+                HttpResponse<Void> response = client.send(request, discard(null));
+                String msg = String.format("UNEXPECTED response=%s%n", response);
+                throw new RuntimeException(msg);
+            } catch (SSLHandshakeException expected) {
+                out.printf("Client: caught expected exception: %s%n", expected);
+            }
+        }
+    }
+
+    void testAsyncSameClient(URI uri, Version version) throws Exception {
+        out.printf("%n--- testAsyncSameClient %s ---%n", version);
+        HttpClient client = HttpClient.newHttpClient();
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            HttpRequest request = HttpRequest.newBuilder(uri)
+                                             .version(version)
+                                             .build();
+            CompletableFuture<HttpResponse<Void>> response =
+                        client.sendAsync(request, discard(null));
+            try {
+                response.join();
+                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;
+                }
+            }
+        }
+    }
+
+    void testAsyncDiffClient(URI uri, Version version) throws Exception {
+        out.printf("%n--- testAsyncDiffClient %s ---%n", version);
+        for (int i = 0; i < TIMES; i++) {
+            out.printf("iteration %d%n", i);
+            // a new client each time
+            HttpClient client = HttpClient.newHttpClient();
+            HttpRequest request = HttpRequest.newBuilder(uri)
+                                             .version(version)
+                                             .build();
+            CompletableFuture<HttpResponse<Void>> response =
+                    client.sendAsync(request, discard(null));
+            try {
+                response.join();
+                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;
+                }
+            }
+        }
+    }
+
+    /** Common supertype for PlainServer and SSLServer. */
+    static abstract class AbstractServer extends Thread implements AutoCloseable {
+        protected final ServerSocket ss;
+        protected volatile boolean closed;
+
+        AbstractServer(String name, ServerSocket ss) throws IOException {
+            super(name);
+            this.ss = ss;
+            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);
+            }
+        }
+    }
+
+    /** Emulates a server-side, using plain cleartext Sockets, that just closes
+     * the connection, after a small variable delay. */
+    static class PlainServer extends AbstractServer {
+        private volatile int count;
+
+        PlainServer() throws IOException {
+            super("PlainServer", new ServerSocket(0));
+        }
+
+        @Override
+        public void run() {
+            while (!closed) {
+                try (Socket s = ss.accept()) {
+                    count++;
+
+                    /*   SSL record layer - contains the client hello
+                    struct {
+                        uint8 major, minor;
+                    } ProtocolVersion;
+
+                    enum {
+                        change_cipher_spec(20), alert(21), handshake(22),
+                        application_data(23), (255)
+                    } ContentType;
+
+                    struct {
+                        ContentType type;
+                        ProtocolVersion version;
+                        uint16 length;
+                        opaque fragment[SSLPlaintext.length];
+                    } SSLPlaintext;   */
+                    DataInputStream din =  new DataInputStream(s.getInputStream());
+                    int contentType = din.read();
+                    out.println("ContentType:" + contentType);
+                    int majorVersion = din.read();
+                    out.println("Major:" + majorVersion);
+                    int minorVersion = din.read();
+                    out.println("Minor:" + minorVersion);
+                    int length = din.readShort();
+                    out.println("length:" + length);
+                    byte[] ba = new byte[length];
+                    din.readFully(ba);
+
+                    // simulate various delays in response
+                    Thread.sleep(10 * (count % 10));
+                    s.close(); // close without giving any reply
+                } catch (IOException e) {
+                    if (!closed)
+                        out.println("Unexpected" + e);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+    }
+
+    /** Emulates a server-side, using SSL Sockets, that will fail during
+     * handshaking, as there are no cipher suites in common. */
+    static class SSLServer extends AbstractServer {
+        static final SSLContext sslContext = createUntrustingContext();
+        static final ServerSocketFactory factory = sslContext.getServerSocketFactory();
+
+        static SSLContext createUntrustingContext() {
+            try {
+                SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+                sslContext.init(null, null, null);
+                return sslContext;
+            } catch (Throwable t) {
+                throw new AssertionError(t);
+            }
+        }
+
+        SSLServer() throws IOException {
+            super("SSLServer", factory.createServerSocket(0));
+        }
+
+        @Override
+        public void run() {
+            while (!closed) {
+                try (SSLSocket s = (SSLSocket)ss.accept()) {
+                    s.getInputStream().read();  // will throw SHE here
+
+                    throw new AssertionError("Should not reach here");
+                } catch (SSLHandshakeException expected) {
+                    // Expected: SSLHandshakeException: no cipher suites in common
+                    out.printf("Server: caught expected exception: %s%n", expected);
+                } catch (IOException e) {
+                    if (!closed)
+                        out.printf("UNEXPECTED %s", e);
+                }
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HeadersTest2.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2015, 2016, 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
+ * @bug 8087112
+ * @summary Basic test for headers
+ */
+
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import java.net.URI;
+import java.util.List;
+import java.util.Iterator;
+
+public class HeadersTest2 {
+    static URI uri = URI.create("http://www.foo.com/");
+
+    static class CompareTest {
+        boolean succeed;
+        List<String> nameValues1;
+        List<String> nameValues2;
+
+
+        /**
+         * Each list contains header-name, header-value, header-name, header-value
+         * sequences. The test creates two requests with the two lists
+         * and compares the HttpHeaders objects returned from the requests
+         *
+         * @param succeed
+         * @param l1
+         * @param l2
+         */
+        CompareTest(boolean succeed, List<String> l1, List<String> l2) {
+            this.succeed = succeed;
+            this.nameValues1 = l1;
+            this.nameValues2 = l2;
+        }
+
+        public void run() {
+            HttpRequest r1 = getRequest(nameValues1);
+            HttpRequest r2 = getRequest(nameValues2);
+            HttpHeaders h1 = r1.headers();
+            HttpHeaders h2 = r2.headers();
+            boolean equal = h1.equals(h2);
+            if (equal && !succeed) {
+                System.err.println("Expected failure");
+                print(nameValues1);
+                print(nameValues2);
+                throw new RuntimeException();
+            } else if (!equal && succeed) {
+                System.err.println("Expected success");
+                print(nameValues1);
+                print(nameValues2);
+                throw new RuntimeException();
+            }
+        }
+
+        static void print(List<String> list) {
+            System.err.print("{");
+            for (String s : list) {
+                System.err.print(s + " ");
+            }
+            System.err.println("}");
+        }
+
+        HttpRequest getRequest(List<String> headers) {
+            HttpRequest.Builder builder = HttpRequest.newBuilder(uri);
+            Iterator<String> iterator = headers.iterator();
+            while (iterator.hasNext()) {
+                String name = iterator.next();
+                String value = iterator.next();
+                builder.header(name, value);
+            }
+            return builder.GET().build();
+        }
+    }
+
+    static CompareTest test(boolean s, List<String> l1, List<String> l2) {
+        return new CompareTest(s, l1, l2);
+    }
+
+    static CompareTest[] compareTests = new CompareTest[] {
+        test(true, List.of("Dontent-length", "99"), List.of("dontent-length", "99")),
+        test(false, List.of("Dontent-length", "99"), List.of("dontent-length", "100")),
+        test(false, List.of("Name1", "val1", "Name1", "val2", "name1", "val3"),
+                    List.of("Name1", "val1", "Name1", "val2")),
+        test(true, List.of("Name1", "val1", "Name1", "val2", "name1", "val3"),
+                   List.of("NaMe1", "val1", "NAme1", "val2", "name1", "val3"))
+    };
+
+    public static void main(String[] args) {
+        for (CompareTest test : compareTests) {
+            test.run();
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2017, 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.lang.reflect.Method;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.util.List;
+import java.util.concurrent.Executor;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Redirect;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+/*
+ * @test
+ * @summary HttpClient[.Builder] API and behaviour checks
+ * @library /lib/testlibrary/
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng HttpClientBuilderTest
+ */
+
+public class HttpClientBuilderTest {
+
+    @Test
+    public void testDefaults() throws Exception {
+        List<HttpClient> clients = List.of(HttpClient.newHttpClient(),
+                                           HttpClient.newBuilder().build());
+
+        for (HttpClient client : clients) {
+            // Empty optionals and defaults
+            assertFalse(client.authenticator().isPresent());
+            assertFalse(client.cookieManager().isPresent());
+            assertFalse(client.executor().isPresent());
+            assertFalse(client.proxy().isPresent());
+            assertTrue(client.sslParameters() != null);
+            assertTrue(client.followRedirects().equals(HttpClient.Redirect.NEVER));
+            assertTrue(client.sslContext() == SSLContext.getDefault());
+            assertTrue(client.version().equals(HttpClient.Version.HTTP_2));
+        }
+    }
+
+    @Test
+    public void testNull() throws Exception {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        assertThrows(NullPointerException.class, () -> builder.authenticator(null));
+        assertThrows(NullPointerException.class, () -> builder.cookieManager(null));
+        assertThrows(NullPointerException.class, () -> builder.executor(null));
+        assertThrows(NullPointerException.class, () -> builder.proxy(null));
+        assertThrows(NullPointerException.class, () -> builder.sslParameters(null));
+        assertThrows(NullPointerException.class, () -> builder.followRedirects(null));
+        assertThrows(NullPointerException.class, () -> builder.sslContext(null));
+        assertThrows(NullPointerException.class, () -> builder.version(null));
+    }
+
+    static class TestAuthenticator extends Authenticator { }
+
+    @Test
+    public void testAuthenticator() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        Authenticator a = new TestAuthenticator();
+        builder.authenticator(a);
+        assertTrue(builder.build().authenticator().get() == a);
+        Authenticator b = new TestAuthenticator();
+        builder.authenticator(b);
+        assertTrue(builder.build().authenticator().get() == b);
+        assertThrows(NullPointerException.class, () -> builder.authenticator(null));
+        Authenticator c = new TestAuthenticator();
+        builder.authenticator(c);
+        assertTrue(builder.build().authenticator().get() == c);
+    }
+
+    @Test
+    public void testCookieManager() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        CookieManager a = new CookieManager();
+        builder.cookieManager(a);
+        assertTrue(builder.build().cookieManager().get() == a);
+        CookieManager b = new CookieManager();
+        builder.cookieManager(b);
+        assertTrue(builder.build().cookieManager().get() == b);
+        assertThrows(NullPointerException.class, () -> builder.cookieManager(null));
+        CookieManager c = new CookieManager();
+        builder.cookieManager(c);
+        assertTrue(builder.build().cookieManager().get() == c);
+    }
+
+    static class TestExecutor implements Executor {
+        public void execute(Runnable r) { }
+    }
+
+    @Test
+    public void testExecutor() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        TestExecutor a = new TestExecutor();
+        builder.executor(a);
+        assertTrue(builder.build().executor().get() == a);
+        TestExecutor b = new TestExecutor();
+        builder.executor(b);
+        assertTrue(builder.build().executor().get() == b);
+        assertThrows(NullPointerException.class, () -> builder.executor(null));
+        TestExecutor c = new TestExecutor();
+        builder.executor(c);
+        assertTrue(builder.build().executor().get() == c);
+    }
+
+    @Test
+    public void testProxySelector() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        ProxySelector a = ProxySelector.of(null);
+        builder.proxy(a);
+        assertTrue(builder.build().proxy().get() == a);
+        ProxySelector b = ProxySelector.of(InetSocketAddress.createUnresolved("foo", 80));
+        builder.proxy(b);
+        assertTrue(builder.build().proxy().get() == b);
+        assertThrows(NullPointerException.class, () -> builder.proxy(null));
+        ProxySelector c = ProxySelector.of(InetSocketAddress.createUnresolved("bar", 80));
+        builder.proxy(c);
+        assertTrue(builder.build().proxy().get() == c);
+    }
+
+    @Test
+    public void testSSLParameters() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        SSLParameters a = new SSLParameters();
+        a.setCipherSuites(new String[] { "A" });
+        builder.sslParameters(a);
+        a.setCipherSuites(new String[] { "Z" });
+        assertTrue(builder.build().sslParameters() != (a));
+        assertTrue(builder.build().sslParameters().getCipherSuites()[0].equals("A"));
+        SSLParameters b = new SSLParameters();
+        b.setEnableRetransmissions(true);
+        builder.sslParameters(b);
+        assertTrue(builder.build().sslParameters() != b);
+        assertTrue(builder.build().sslParameters().getEnableRetransmissions());
+        assertThrows(NullPointerException.class, () -> builder.sslParameters(null));
+        SSLParameters c = new SSLParameters();
+        c.setProtocols(new String[] { "C" });
+        builder.sslParameters(c);
+        c.setProtocols(new String[] { "D" });
+        assertTrue(builder.build().sslParameters().getProtocols()[0].equals("C"));
+    }
+
+    @Test
+    public void testSSLContext() throws Exception {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        SSLContext a = (new SimpleSSLContext()).get();
+        builder.sslContext(a);
+        assertTrue(builder.build().sslContext() == a);
+        SSLContext b = (new SimpleSSLContext()).get();
+        builder.sslContext(b);
+        assertTrue(builder.build().sslContext() == b);
+        assertThrows(NullPointerException.class, () -> builder.sslContext(null));
+        SSLContext c = (new SimpleSSLContext()).get();
+        builder.sslContext(c);
+        assertTrue(builder.build().sslContext() == c);
+    }
+
+    @Test
+    public void testFollowRedirects() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        builder.followRedirects(Redirect.ALWAYS);
+        assertTrue(builder.build().followRedirects() == Redirect.ALWAYS);
+        builder.followRedirects(Redirect.NEVER);
+        assertTrue(builder.build().followRedirects() == Redirect.NEVER);
+        assertThrows(NullPointerException.class, () -> builder.followRedirects(null));
+        builder.followRedirects(Redirect.SAME_PROTOCOL);
+        assertTrue(builder.build().followRedirects() == Redirect.SAME_PROTOCOL);
+        builder.followRedirects(Redirect.SECURE);
+        assertTrue(builder.build().followRedirects() == Redirect.SECURE);
+    }
+
+    @Test
+    public void testVersion() {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        builder.version(Version.HTTP_2);
+        assertTrue(builder.build().version() == Version.HTTP_2);
+        builder.version(Version.HTTP_1_1);
+        assertTrue(builder.build().version() == Version.HTTP_1_1);
+        assertThrows(NullPointerException.class, () -> builder.version(null));
+        builder.version(Version.HTTP_2);
+        assertTrue(builder.build().version() == Version.HTTP_2);
+        builder.version(Version.HTTP_1_1);
+        assertTrue(builder.build().version() == Version.HTTP_1_1);
+    }
+
+    @Test
+    static void testPriority() throws Exception {
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        assertThrows(IllegalArgumentException.class, () -> builder.priority(-1));
+        assertThrows(IllegalArgumentException.class, () -> builder.priority(0));
+        assertThrows(IllegalArgumentException.class, () -> builder.priority(257));
+        assertThrows(IllegalArgumentException.class, () -> builder.priority(500));
+
+        builder.priority(1);
+        builder.build();
+        builder.priority(256);
+        builder.build();
+    }
+
+
+    /* ---- standalone entry point ---- */
+
+    public static void main(String[] args) throws Exception {
+        HttpClientBuilderTest test = new HttpClientBuilderTest();
+        for (Method m : HttpClientBuilderTest.class.getDeclaredMethods()) {
+            if (m.isAnnotationPresent(Test.class)) {
+                try {
+                    m.invoke(test);
+                    System.out.printf("test %s: success%n", m.getName());
+                } catch (Throwable t ) {
+                    System.out.printf("test %s: failed%n", m.getName());
+                    t.printStackTrace();
+                }
+            }
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/HttpInputStreamTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/HttpInputStreamTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -32,6 +32,8 @@
 import jdk.incubator.http.HttpResponse;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -40,11 +42,12 @@
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.Flow;
 import java.util.stream.Stream;
+import static java.lang.System.err;
 
 /*
  * @test
  * @summary An example on how to read a response body with InputStream...
- * @run main/othervm HttpInputStreamTest
+ * @run main/othervm -Dtest.debug=true HttpInputStreamTest
  * @author daniel fuchs
  */
 public class HttpInputStreamTest {
@@ -61,7 +64,7 @@
     public static class HttpInputStreamHandler
         implements HttpResponse.BodyHandler<InputStream>    {
 
-        public static final int MAX_BUFFERS_IN_QUEUE = 1;
+        public static final int MAX_BUFFERS_IN_QUEUE = 1;  // lock-step with the producer
 
         private final int maxBuffers;
 
@@ -74,7 +77,7 @@
         }
 
         @Override
-        public synchronized HttpResponse.BodyProcessor<InputStream>
+        public HttpResponse.BodySubscriber<InputStream>
                 apply(int i, HttpHeaders hh) {
             return new HttpResponseInputStream(maxBuffers);
         }
@@ -83,17 +86,19 @@
          * An InputStream built on top of the Flow API.
          */
         private static class HttpResponseInputStream extends InputStream
-                    implements HttpResponse.BodyProcessor<InputStream> {
+                    implements HttpResponse.BodySubscriber<InputStream> {
 
             // An immutable ByteBuffer sentinel to mark that the last byte was received.
-            private static final ByteBuffer LAST = ByteBuffer.wrap(new byte[0]);
+            private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]);
+            private static final List<ByteBuffer> LAST_LIST = List.of(LAST_BUFFER);
 
             // A queue of yet unprocessed ByteBuffers received from the flow API.
-            private final BlockingQueue<ByteBuffer> buffers;
+            private final BlockingQueue<List<ByteBuffer>> buffers;
             private volatile Flow.Subscription subscription;
             private volatile boolean closed;
             private volatile Throwable failed;
-            private volatile ByteBuffer current;
+            private volatile Iterator<ByteBuffer> currentListItr;
+            private volatile ByteBuffer currentBuffer;
 
             HttpResponseInputStream() {
                 this(MAX_BUFFERS_IN_QUEUE);
@@ -101,7 +106,8 @@
 
             HttpResponseInputStream(int maxBuffers) {
                 int capacity = maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers;
-                this.buffers = new ArrayBlockingQueue<>(capacity);
+                // 1 additional slot for LAST_LIST added by onComplete
+                this.buffers = new ArrayBlockingQueue<>(capacity + 1);
             }
 
             @Override
@@ -119,40 +125,49 @@
             // a new buffer is made available through the Flow API, or the
             // end of the flow is reached.
             private ByteBuffer current() throws IOException {
-                while (current == null || !current.hasRemaining()) {
-                    // Check whether the stream is claused or exhausted
+                while (currentBuffer == null || !currentBuffer.hasRemaining()) {
+                    // Check whether the stream is closed or exhausted
                     if (closed || failed != null) {
                         throw new IOException("closed", failed);
                     }
-                    if (current == LAST) break;
+                    if (currentBuffer == LAST_BUFFER) break;
 
                     try {
-                        // Take a new buffer from the queue, blocking
-                        // if none is available yet...
-                        if (DEBUG) System.err.println("Taking Buffer");
-                        current = buffers.take();
-                        if (DEBUG) System.err.println("Buffer Taken");
+                        if (currentListItr == null || !currentListItr.hasNext()) {
+                            // Take a new list of buffers from the queue, blocking
+                            // if none is available yet...
+
+                            if (DEBUG) err.println("Taking list of Buffers");
+                            List<ByteBuffer> lb = buffers.take();
+                            currentListItr = lb.iterator();
+                            if (DEBUG) err.println("List of Buffers Taken");
+
+                            // Check whether an exception was encountered upstream
+                            if (closed || failed != null)
+                                throw new IOException("closed", failed);
 
-                        // Check whether some exception was encountered
-                        // upstream
-                        if (closed || failed != null) {
-                            throw new IOException("closed", failed);
-                        }
+                            // Check whether we're done.
+                            if (lb == LAST_LIST) {
+                                currentListItr = null;
+                                currentBuffer = LAST_BUFFER;
+                                break;
+                            }
 
-                        // Check whether we're done.
-                        if (current == LAST) break;
-
-                        // Inform the producer that it can start sending
-                        // us a new buffer
-                        Flow.Subscription s = subscription;
-                        if (s != null) s.request(1);
-
+                            // Request another upstream item ( list of buffers )
+                            Flow.Subscription s = subscription;
+                            if (s != null)
+                                s.request(1);
+                        }
+                        assert currentListItr != null;
+                        assert currentListItr.hasNext();
+                        if (DEBUG) err.println("Next Buffer");
+                        currentBuffer = currentListItr.next();
                     } catch (InterruptedException ex) {
                         // continue
                     }
                 }
-                assert current == LAST || current.hasRemaining();
-                return current;
+                assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining();
+                return currentBuffer;
             }
 
             @Override
@@ -160,7 +175,7 @@
                 // get the buffer to read from, possibly blocking if
                 // none is available
                 ByteBuffer buffer;
-                if ((buffer = current()) == LAST) return -1;
+                if ((buffer = current()) == LAST_BUFFER) return -1;
 
                 // don't attempt to read more than what is available
                 // in the current buffer.
@@ -175,22 +190,31 @@
             @Override
             public int read() throws IOException {
                 ByteBuffer buffer;
-                if ((buffer = current()) == LAST) return -1;
+                if ((buffer = current()) == LAST_BUFFER) return -1;
                 return buffer.get() & 0xFF;
             }
 
             @Override
             public void onSubscribe(Flow.Subscription s) {
+                if (this.subscription != null) {
+                    s.cancel();
+                    return;
+                }
                 this.subscription = s;
-                s.request(Math.max(2, buffers.remainingCapacity() + 1));
+                assert buffers.remainingCapacity() > 1; // should at least be 2
+                if (DEBUG) err.println("onSubscribe: requesting "
+                     + Math.max(1, buffers.remainingCapacity() - 1));
+                s.request(Math.max(1, buffers.remainingCapacity() - 1));
             }
 
             @Override
-            public synchronized void onNext(ByteBuffer t) {
+            public void onNext(List<ByteBuffer> t) {
                 try {
-                    if (DEBUG) System.err.println("next buffer received");
-                    buffers.put(t);
-                    if (DEBUG) System.err.println("buffered offered");
+                    if (DEBUG) err.println("next item received");
+                    if (!buffers.offer(t)) {
+                        throw new IllegalStateException("queue is full");
+                    }
+                    if (DEBUG) err.println("item offered");
                 } catch (Exception ex) {
                     failed = ex;
                     try {
@@ -203,24 +227,26 @@
 
             @Override
             public void onError(Throwable thrwbl) {
+                subscription = null;
                 failed = thrwbl;
             }
 
             @Override
-            public synchronized void onComplete() {
+            public void onComplete() {
                 subscription = null;
-                onNext(LAST);
+                onNext(LAST_LIST);
             }
 
             @Override
             public void close() throws IOException {
                 synchronized (this) {
+                    if (closed) return;
                     closed = true;
-                    Flow.Subscription s = subscription;
-                    if (s != null) {
-                        s.cancel();
-                    }
-                    subscription = null;
+                }
+                Flow.Subscription s = subscription;
+                subscription = null;
+                if (s != null) {
+                     s.cancel();
                 }
                 super.close();
             }
@@ -274,8 +300,8 @@
         //    client.sendAsync(request, HttpResponse.BodyHandler.asString()).get().body());
 
         CompletableFuture<HttpResponse<InputStream>> handle =
-            client.sendAsync(request, new HttpInputStreamHandler());
-        if (DEBUG) System.err.println("Request sent");
+            client.sendAsync(request, new HttpInputStreamHandler(3));
+        if (DEBUG) err.println("Request sent");
 
         HttpResponse<InputStream> pending = handle.get();
 
@@ -301,8 +327,8 @@
 
             char[] buff = new char[32];
             int off=0, n=0;
-            if (DEBUG) System.err.println("Start receiving response body");
-            if (DEBUG) System.err.println("Charset: " + charset.get());
+            if (DEBUG) err.println("Start receiving response body");
+            if (DEBUG) err.println("Charset: " + charset.get());
 
             // Start consuming the InputStream as the data arrives.
             // Will block until there is something to read...
--- a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -21,21 +21,22 @@
  * questions.
  */
 
-import jdk.incubator.http.HttpRequest;
 import java.net.URI;
 import jdk.incubator.http.HttpClient;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jdk.incubator.http.HttpRequest;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 
 /**
  * @test
  * @bug 8170064
- * @summary  HttpRequest API documentation says:" Unless otherwise stated,
- * {@code null} parameter values will cause methods
- * of this class to throw {@code NullPointerException}".
+ * @summary  HttpRequest[.Builder] API and behaviour checks
  */
 public class HttpRequestBuilderTest {
 
@@ -44,70 +45,166 @@
 
     public static void main(String[] args) throws Exception {
 
+        test0("newBuilder().build()",
+              () -> HttpRequest.newBuilder().build(),
+              IllegalStateException.class);
+
+        test0("newBuilder(null)",
+              () -> HttpRequest.newBuilder(null),
+              NullPointerException.class);
+
+        test0("newBuilder(URI.create(\"badScheme://www.foo.com/\")",
+              () -> HttpRequest.newBuilder(URI.create("badScheme://www.foo.com/")),
+              IllegalArgumentException.class);
+
+        test0("newBuilder(URI.create(\"http://www.foo.com:-1/\")",
+                () -> HttpRequest.newBuilder(URI.create("http://www.foo.com:-1/")),
+                IllegalArgumentException.class);
+
+        test0("newBuilder(URI.create(\"https://www.foo.com:-1/\")",
+                () -> HttpRequest.newBuilder(URI.create("https://www.foo.com:-1/")),
+                IllegalArgumentException.class);
+
+        test0("newBuilder(" + TEST_URI + ").uri(null)",
+              () -> HttpRequest.newBuilder(TEST_URI).uri(null),
+              NullPointerException.class);
+
+        test0("newBuilder(uri).build()",
+              () -> HttpRequest.newBuilder(TEST_URI).build()
+              /* no expected exceptions */ );
+
         HttpRequest.Builder builder = HttpRequest.newBuilder();
+
         builder = test1("uri", builder, builder::uri, (URI)null,
                         NullPointerException.class);
+
+        builder = test1("uri", builder, builder::uri, URI.create("http://www.foo.com:-1/"),
+                        IllegalArgumentException.class);
+
+        builder = test1("uri", builder, builder::uri, URI.create("https://www.foo.com:-1/"),
+                        IllegalArgumentException.class);
+
         builder = test2("header", builder, builder::header, (String) null, "bar",
                         NullPointerException.class);
+
         builder = test2("header", builder, builder::header, "foo", (String) null,
                         NullPointerException.class);
+
         builder = test2("header", builder, builder::header, (String)null,
                         (String) null, NullPointerException.class);
+
+        builder = test2("header", builder, builder::header, "", "bar",
+                        IllegalArgumentException.class);
+
+        builder = test2("header", builder, builder::header, "foo", "\r",
+                        IllegalArgumentException.class);
+
         builder = test1("headers", builder, builder::headers, (String[]) null,
                         NullPointerException.class);
+
+        builder = test1("headers", builder, builder::headers, new String[0],
+                        IllegalArgumentException.class);
+
         builder = test1("headers", builder, builder::headers,
                         (String[]) new String[] {null, "bar"},
                         NullPointerException.class);
+
         builder = test1("headers", builder, builder::headers,
                         (String[]) new String[] {"foo", null},
                         NullPointerException.class);
+
         builder = test1("headers", builder, builder::headers,
                         (String[]) new String[] {null, null},
                         NullPointerException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo", "bar", null},
-                       NullPointerException.class,
-                       IllegalArgumentException.class);
+                        (String[]) new String[] {"foo", "bar", null},
+                        NullPointerException.class,
+                        IllegalArgumentException.class);
+
+        builder = test1("headers", builder, builder::headers,
+                        (String[]) new String[] {"foo", "bar", null, null},
+                        NullPointerException.class);
+
+        builder = test1("headers", builder, builder::headers,
+                        (String[]) new String[] {"foo", "bar", "baz", null},
+                        NullPointerException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo", "bar", null, null},
-                       NullPointerException.class);
+                        (String[]) new String[] {"foo", "bar", "\r", "baz"},
+                        IllegalArgumentException.class);
+
+        builder = test1("headers", builder, builder::headers,
+                        (String[]) new String[] {"foo", "bar", "baz", "\n"},
+                        IllegalArgumentException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo", "bar", "baz", null},
-                       NullPointerException.class);
+                        (String[]) new String[] {"foo", "bar", "", "baz"},
+                        IllegalArgumentException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo", "bar", null, "baz"},
-                       NullPointerException.class);
+                        (String[]) new String[] {"foo", "bar", null, "baz"},
+                        NullPointerException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo", "bar", "baz"},
-                       IllegalArgumentException.class);
+                        (String[]) new String[] {"foo", "bar", "baz"},
+                        IllegalArgumentException.class);
+
         builder = test1("headers", builder, builder::headers,
-                       (String[]) new String[] {"foo"},
-                       IllegalArgumentException.class);
+                        (String[]) new String[] {"foo"},
+                        IllegalArgumentException.class);
+
         builder = test1("DELETE", builder, builder::DELETE,
-                        (HttpRequest.BodyProcessor)null, null);
+                        HttpRequest.noBody(), null);
+
         builder = test1("POST", builder, builder::POST,
-                        (HttpRequest.BodyProcessor)null, null);
+                        HttpRequest.noBody(), null);
+
         builder = test1("PUT", builder, builder::PUT,
-                        (HttpRequest.BodyProcessor)null, null);
+                        HttpRequest.noBody(), null);
+
         builder = test2("method", builder, builder::method, "GET",
-                        (HttpRequest.BodyProcessor) null, null);
+                        HttpRequest.noBody(), null);
+
+        builder = test1("DELETE", builder, builder::DELETE,
+                        (HttpRequest.BodyPublisher)null,
+                        NullPointerException.class);
+
+        builder = test1("POST", builder, builder::POST,
+                        (HttpRequest.BodyPublisher)null,
+                        NullPointerException.class);
+
+        builder = test1("PUT", builder, builder::PUT,
+                        (HttpRequest.BodyPublisher)null,
+                        NullPointerException.class);
+
+        builder = test2("method", builder, builder::method, "GET",
+                        (HttpRequest.BodyPublisher) null,
+                        NullPointerException.class);
+
         builder = test2("setHeader", builder, builder::setHeader,
                         (String) null, "bar",
                         NullPointerException.class);
+
         builder = test2("setHeader", builder, builder::setHeader,
                         "foo", (String) null,
                         NullPointerException.class);
+
         builder = test2("setHeader", builder, builder::setHeader,
                         (String)null, (String) null,
                         NullPointerException.class);
+
         builder = test1("timeout", builder, builder::timeout,
-                        (Duration)null, NullPointerException.class);
+                        (Duration)null,
+                        NullPointerException.class);
+
         builder = test1("version", builder, builder::version,
                         (HttpClient.Version)null,
                         NullPointerException.class);
+
         builder = test2("method", builder, builder::method, null,
-                       HttpRequest.BodyProcessor.fromString("foo"),
-                       NullPointerException.class);
+                        HttpRequest.BodyPublisher.fromString("foo"),
+                        NullPointerException.class);
 // see JDK-8170093
 //
 //        builder = test2("method", builder, builder::method, "foo",
@@ -116,6 +213,53 @@
 //
 //        builder.build();
 
+
+        method("newBuilder(TEST_URI).build().method() == GET",
+               () -> HttpRequest.newBuilder(TEST_URI),
+               "GET");
+
+        method("newBuilder(TEST_URI).GET().build().method() == GET",
+               () -> HttpRequest.newBuilder(TEST_URI).GET(),
+               "GET");
+
+        method("newBuilder(TEST_URI).POST(fromString(\"\")).GET().build().method() == GET",
+               () -> HttpRequest.newBuilder(TEST_URI).POST(fromString("")).GET(),
+               "GET");
+
+        method("newBuilder(TEST_URI).PUT(fromString(\"\")).GET().build().method() == GET",
+               () -> HttpRequest.newBuilder(TEST_URI).PUT(fromString("")).GET(),
+               "GET");
+
+        method("newBuilder(TEST_URI).DELETE(fromString(\"\")).GET().build().method() == GET",
+               () -> HttpRequest.newBuilder(TEST_URI).DELETE(fromString("")).GET(),
+               "GET");
+
+        method("newBuilder(TEST_URI).POST(fromString(\"\")).build().method() == POST",
+               () -> HttpRequest.newBuilder(TEST_URI).POST(fromString("")),
+               "POST");
+
+        method("newBuilder(TEST_URI).PUT(fromString(\"\")).build().method() == PUT",
+               () -> HttpRequest.newBuilder(TEST_URI).PUT(fromString("")),
+               "PUT");
+
+        method("newBuilder(TEST_URI).DELETE(fromString(\"\")).build().method() == DELETE",
+               () -> HttpRequest.newBuilder(TEST_URI).DELETE(fromString("")),
+               "DELETE");
+
+        method("newBuilder(TEST_URI).GET().POST(fromString(\"\")).build().method() == POST",
+               () -> HttpRequest.newBuilder(TEST_URI).GET().POST(fromString("")),
+               "POST");
+
+        method("newBuilder(TEST_URI).GET().PUT(fromString(\"\")).build().method() == PUT",
+               () -> HttpRequest.newBuilder(TEST_URI).GET().PUT(fromString("")),
+               "PUT");
+
+        method("newBuilder(TEST_URI).GET().DELETE(fromString(\"\")).build().method() == DELETE",
+               () -> HttpRequest.newBuilder(TEST_URI).GET().DELETE(fromString("")),
+               "DELETE");
+
+
+
     }
 
     private static boolean shouldFail(Class<? extends Exception> ...exceptions) {
@@ -133,22 +277,66 @@
                 .findAny().isPresent();
     }
 
-    public static <R,P> R test1(String name, R receiver, Function<P, R> m, P arg,
-                               Class<? extends Exception> ...ex) {
+    static void method(String name,
+                       Supplier<HttpRequest.Builder> supplier,
+                       String expectedMethod) {
+        HttpRequest request = supplier.get().build();
+        String method = request.method();
+        if (request.method().equals("GET") && request.bodyPublisher().isPresent())
+            throw new AssertionError("failed: " + name
+                    + ". Unexpected body processor for GET: "
+                    + request.bodyPublisher().get());
+
+        if (expectedMethod.equals(method)) {
+            System.out.println("success: " + name);
+        } else {
+            throw new AssertionError("failed: " + name
+                    + ". Expected " + expectedMethod + ", got " + method);
+        }
+    }
+
+    static void test0(String name,
+                      Runnable r,
+                      Class<? extends Exception> ...ex) {
         try {
-            R result =  m.apply(arg);
+            r.run();
             if (!shouldFail(ex)) {
-                System.out.println("success: " + name + "(" + arg + ")");
-                return result;
+                System.out.println("success: " + name);
+                return;
             } else {
                 throw new AssertionError("Expected " + expectedNames(ex)
-                    + " not raised for " + name + "(" + arg + ")");
+                        + " not raised for " + name);
             }
         } catch (Exception x) {
             if (!isExpected(x, ex)) {
                 throw x;
             } else {
-                System.out.println("success: " + name + "(" + arg + ")" +
+                System.out.println("success: " + name +
+                        " - Got expected exception: " + x);
+            }
+        }
+    }
+
+    public static <R,P> R test1(String name, R receiver, Function<P, R> m, P arg,
+                               Class<? extends Exception> ...ex) {
+        String argMessage = arg == null ? "null" : arg.toString();
+        if (arg instanceof String[]) {
+            argMessage = Arrays.asList((String[])arg).toString();
+        }
+        try {
+            R result =  m.apply(arg);
+            if (!shouldFail(ex)) {
+                System.out.println("success: " + name + "(" + argMessage + ")");
+                return result;
+            } else {
+                throw new AssertionError("Expected " + expectedNames(ex)
+                    + " not raised for " + name + "(" + argMessage + ")");
+            }
+        } catch (Exception x) {
+            if (!isExpected(x, ex)) {
+                throw x;
+            } else {
+                System.out.println("success: " + name + "(" + argMessage + ")" +
                         " - Got expected exception: " + x);
                 return receiver;
             }
--- a/test/jdk/java/net/httpclient/LightWeightHttpServer.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/LightWeightHttpServer.java	Sun Nov 05 17:32:13 2017 +0000
@@ -80,7 +80,7 @@
         ch.setLevel(Level.ALL);
         logger.addHandler(ch);
 
-        String root = System.getProperty("test.src") + "/docs";
+        String root = System.getProperty("test.src", ".") + "/docs";
         InetSocketAddress addr = new InetSocketAddress(0);
         httpServer = HttpServer.create(addr, 0);
         if (httpServer instanceof HttpsServer) {
@@ -301,11 +301,12 @@
 
         @Override
         public synchronized void handle(HttpExchange he) throws IOException {
-            byte[] buf = Util.readAll(he.getRequestBody());
-            try {
+            try(InputStream is = he.getRequestBody()) {
+                is.readAllBytes();
                 bar1.await();
                 bar2.await();
             } catch (InterruptedException | BrokenBarrierException e) {
+                throw new IOException(e);
             }
             he.sendResponseHeaders(200, -1); // will probably fail
             he.close();
--- a/test/jdk/java/net/httpclient/ManyRequests.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequests.java	Sun Nov 05 17:32:13 2017 +0000
@@ -56,13 +56,12 @@
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.Random;
-import java.util.concurrent.ExecutorService;
 import java.util.logging.Logger;
 import java.util.logging.Level;
 import java.util.concurrent.CompletableFuture;
 import javax.net.ssl.SSLContext;
 import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromByteArray;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asByteArray;
 
 public class ManyRequests {
@@ -91,7 +90,6 @@
             System.out.println("OK");
         } finally {
             server.stop(0);
-            ((ExecutorService)client.executor()).shutdownNow();
         }
     }
 
@@ -111,15 +109,17 @@
         }
         protected void close(OutputStream os) throws IOException {
             if (INSERT_DELAY) {
-                try { Thread.sleep(rand.nextInt(200)); } catch (InterruptedException e) {}
+                try { Thread.sleep(rand.nextInt(200)); }
+                catch (InterruptedException e) {}
             }
-            super.close(os);
+            os.close();
         }
         protected void close(InputStream is) throws IOException {
             if (INSERT_DELAY) {
-                try { Thread.sleep(rand.nextInt(200)); } catch (InterruptedException e) {}
+                try { Thread.sleep(rand.nextInt(200)); }
+                catch (InterruptedException e) {}
             }
-            super.close(is);
+            is.close();
         }
     }
 
--- a/test/jdk/java/net/httpclient/ManyRequests2.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequests2.java	Sun Nov 05 17:32:13 2017 +0000
@@ -36,7 +36,7 @@
  * @run main/othervm/timeout=40 -Dtest.XFixed=true ManyRequests2
  * @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.insertDelay=true ManyRequests2
  * @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.chunkSize=64 ManyRequests2
- * @run main/othervm/timeout=40 -Dtest.XFixed=true -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequests2
+ * @run main/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Dtest.XFixed=true -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequests2
  * @summary Send a large number of requests asynchronously. The server echoes back using known content length.
  */
  // * @run main/othervm/timeout=40 -Djdk.httpclient.HttpClient.log=ssl ManyRequests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2015, 2017, 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
+ * @modules jdk.incubator.httpclient
+ *          java.logging
+ *          jdk.httpserver
+ * @library /lib/testlibrary/ /
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @compile ../../../com/sun/net/httpserver/LogFilter.java
+ * @compile ../../../com/sun/net/httpserver/EchoHandler.java
+ * @compile ../../../com/sun/net/httpserver/FileServerHandler.java
+ * @run main/othervm/timeout=40 ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.insertDelay=true ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.chunkSize=64 ManyRequestsLegacy
+ * @run main/othervm/timeout=40 -Dtest.insertDelay=true -Dtest.chunkSize=64 ManyRequestsLegacy
+ * @summary Send a large number of requests asynchronously using the legacy URL.openConnection(), to help sanitize results of the test ManyRequest.java.
+ */
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.HostnameVerifier;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsParameters;
+import com.sun.net.httpserver.HttpsServer;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import jdk.testlibrary.SimpleSSLContext;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asByteArray;
+
+public class ManyRequestsLegacy {
+
+    volatile static int counter = 0;
+
+    public static void main(String[] args) throws Exception {
+        Logger logger = Logger.getLogger("com.sun.net.httpserver");
+        logger.setLevel(Level.ALL);
+        logger.info("TEST");
+        System.out.println("Sending " + REQUESTS
+                         + " requests; delay=" + INSERT_DELAY
+                         + ", chunks=" + CHUNK_SIZE
+                         + ", XFixed=" + XFIXED);
+        SSLContext ctx = new SimpleSSLContext().get();
+        SSLContext.setDefault(ctx);
+        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
+                public boolean verify(String hostname, SSLSession session) {
+                    return true;
+                }
+            });
+        InetSocketAddress addr = new InetSocketAddress(0);
+        HttpsServer server = HttpsServer.create(addr, 0);
+        server.setHttpsConfigurator(new Configurator(ctx));
+
+        LegacyHttpClient client = new LegacyHttpClient();
+
+        try {
+            test(server, client);
+            System.out.println("OK");
+        } finally {
+            server.stop(0);
+        }
+    }
+
+    //static final int REQUESTS = 1000;
+    static final int REQUESTS = 20;
+    static final boolean INSERT_DELAY = Boolean.getBoolean("test.insertDelay");
+    static final int CHUNK_SIZE = Math.max(0,
+           Integer.parseInt(System.getProperty("test.chunkSize", "0")));
+    static final boolean XFIXED = Boolean.getBoolean("test.XFixed");
+
+    static class LegacyHttpClient {
+        static final class LegacyHttpResponse extends HttpResponse<byte[]> {
+            final HttpRequest request;
+            final byte[] response;
+            final int statusCode;
+            public LegacyHttpResponse(HttpRequest request, int statusCode, byte[] response) {
+                this.request = request;
+                this.statusCode = statusCode;
+                this.response = response;
+            }
+            private <T> T error() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override
+            public int statusCode() { return statusCode;}
+            @Override
+            public HttpRequest request() {return request;}
+            @Override
+            public HttpRequest finalRequest() {return request;}
+            @Override
+            public HttpHeaders headers() { return error(); }
+            @Override
+            public CompletableFuture<HttpHeaders> trailers() { return error(); }
+            @Override
+            public byte[] body() {return response;}
+            @Override
+            public SSLParameters sslParameters() {
+                try {
+                    return SSLContext.getDefault().getDefaultSSLParameters();
+                } catch (NoSuchAlgorithmException ex) {
+                    throw new UnsupportedOperationException(ex);
+                }
+            }
+            @Override
+            public URI uri() { return request.uri();}
+            @Override
+            public HttpClient.Version version() { return Version.HTTP_1_1;}
+        }
+
+        private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+            System.err.println(tag + " elapsed "
+                    + (System.nanoTime() - startNanos)/1000_000L
+                    + " millis for " + req.method()
+                    + " to " + req.uri());
+        }
+
+        CompletableFuture<? extends HttpResponse<byte[]>> sendAsync(HttpRequest r, byte[] buf) {
+            long start = System.nanoTime();
+            try {
+                CompletableFuture<LegacyHttpResponse> cf = new CompletableFuture<>();
+                URLConnection urlc = r.uri().toURL().openConnection();
+                HttpURLConnection httpc = (HttpURLConnection)urlc;
+                httpc.setRequestMethod(r.method());
+                for (String s : r.headers().map().keySet()) {
+                    httpc.setRequestProperty(s, r.headers().allValues(s)
+                        .stream().collect(Collectors.joining(",")));
+                }
+                httpc.setDoInput(true);
+                if (buf != null) httpc.setDoOutput(true);
+                Thread t = new Thread(() -> {
+                    try {
+                        if (buf != null) {
+                            try (OutputStream os = httpc.getOutputStream()) {
+                                os.write(buf);
+                                os.flush();
+                            }
+                        }
+                        LegacyHttpResponse response = new LegacyHttpResponse(r,
+                                httpc.getResponseCode(),httpc.getInputStream().readAllBytes());
+                        cf.complete(response);
+                    } catch(Throwable x) {
+                        cf.completeExceptionally(x);
+                    }
+                });
+                t.start();
+                return cf.whenComplete((b,x) -> debugCompleted("ClientImpl (async)", start, r));
+            } catch(Throwable t) {
+                debugCompleted("ClientImpl (async)", start, r);
+                return CompletableFuture.failedFuture(t);
+            }
+        }
+    }
+
+    static class TestEchoHandler extends EchoHandler {
+        final Random rand = new Random();
+        @Override
+        public void handle(HttpExchange e) throws IOException {
+            System.out.println("Server: received " + e.getRequestURI());
+            super.handle(e);
+        }
+        @Override
+        protected void close(OutputStream os) throws IOException {
+            if (INSERT_DELAY) {
+                try { Thread.sleep(rand.nextInt(200)); }
+                catch (InterruptedException e) {}
+            }
+            os.close();
+        }
+        @Override
+        protected void close(InputStream is) throws IOException {
+            if (INSERT_DELAY) {
+                try { Thread.sleep(rand.nextInt(200)); }
+                catch (InterruptedException e) {}
+            }
+            is.close();
+        }
+    }
+
+    static void test(HttpsServer server, LegacyHttpClient client) throws Exception {
+        int port = server.getAddress().getPort();
+        URI baseURI = new URI("https://127.0.0.1:" + port + "/foo/x");
+        server.createContext("/foo", new TestEchoHandler());
+        server.start();
+
+        RequestLimiter limiter = new RequestLimiter(40);
+        Random rand = new Random();
+        CompletableFuture<?>[] results = new CompletableFuture<?>[REQUESTS];
+        HashMap<HttpRequest,byte[]> bodies = new HashMap<>();
+
+        for (int i=0; i<REQUESTS; i++) {
+            byte[] buf = new byte[(i+1)*CHUNK_SIZE+i+1];  // different size bodies
+            rand.nextBytes(buf);
+            URI uri = new URI(baseURI.toString() + String.valueOf(i+1));
+            HttpRequest r = HttpRequest.newBuilder(uri)
+                                       .header("XFixed", "true")
+                                       .POST(fromByteArray(buf))
+                                       .build();
+            bodies.put(r, buf);
+
+            results[i] =
+                limiter.whenOkToSend()
+                       .thenCompose((v) -> {
+                           System.out.println("Client: sendAsync: " + r.uri());
+                           return client.sendAsync(r, buf);
+                       })
+                       .thenCompose((resp) -> {
+                           limiter.requestComplete();
+                           if (resp.statusCode() != 200) {
+                               String s = "Expected 200, got: " + resp.statusCode();
+                               System.out.println(s + " from "
+                                                  + resp.request().uri().getPath());
+                               return completedWithIOException(s);
+                           } else {
+                               counter++;
+                               System.out.println("Result (" + counter + ") from "
+                                                   + resp.request().uri().getPath());
+                           }
+                           return CompletableFuture.completedStage(resp.body())
+                                      .thenApply((b) -> new Pair<>(resp, b));
+                       })
+                      .thenAccept((pair) -> {
+                          HttpRequest request = pair.t.request();
+                          byte[] requestBody = bodies.get(request);
+                          check(Arrays.equals(requestBody, pair.u),
+                                "bodies not equal:[" + bytesToHexString(requestBody)
+                                + "] [" + bytesToHexString(pair.u) + "]");
+
+                      });
+        }
+
+        // wait for them all to complete and throw exception in case of error
+        CompletableFuture.allOf(results).join();
+    }
+
+    static <T> CompletableFuture<T> completedWithIOException(String message) {
+        return CompletableFuture.failedFuture(new IOException(message));
+    }
+
+    static String bytesToHexString(byte[] bytes) {
+        if (bytes == null)
+            return "null";
+
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+
+        Formatter formatter = new Formatter(sb);
+        for (byte b : bytes) {
+            formatter.format("%02x", b);
+        }
+
+        return sb.toString();
+    }
+
+    static final class Pair<T,U> {
+        Pair(T t, U u) {
+            this.t = t; this.u = u;
+        }
+        T t;
+        U u;
+    }
+
+    /**
+     * A simple limiter for controlling the number of requests to be run in
+     * parallel whenOkToSend() is called which returns a CF<Void> that allows
+     * each individual request to proceed, or block temporarily (blocking occurs
+     * on the waiters list here. As each request actually completes
+     * requestComplete() is called to notify this object, and allow some
+     * requests to continue.
+     */
+    static class RequestLimiter {
+
+        static final CompletableFuture<Void> COMPLETED_FUTURE =
+                CompletableFuture.completedFuture(null);
+
+        final int maxnumber;
+        final LinkedList<CompletableFuture<Void>> waiters;
+        int number;
+        boolean blocked;
+
+        RequestLimiter(int maximum) {
+            waiters = new LinkedList<>();
+            maxnumber = maximum;
+        }
+
+        synchronized void requestComplete() {
+            number--;
+            // don't unblock until number of requests has halved.
+            if ((blocked && number <= maxnumber / 2) ||
+                        (!blocked && waiters.size() > 0)) {
+                int toRelease = Math.min(maxnumber - number, waiters.size());
+                for (int i=0; i<toRelease; i++) {
+                    CompletableFuture<Void> f = waiters.remove();
+                    number ++;
+                    f.complete(null);
+                }
+                blocked = number >= maxnumber;
+            }
+        }
+
+        synchronized CompletableFuture<Void> whenOkToSend() {
+            if (blocked || number + 1 >= maxnumber) {
+                blocked = true;
+                CompletableFuture<Void> r = new CompletableFuture<>();
+                waiters.add(r);
+                return r;
+            } else {
+                number++;
+                return COMPLETED_FUTURE;
+            }
+        }
+    }
+
+    static void check(boolean cond, Object... msg) {
+        if (cond)
+            return;
+        StringBuilder sb = new StringBuilder();
+        for (Object o : msg)
+            sb.append(o);
+        throw new RuntimeException(sb.toString());
+    }
+
+    static class Configurator extends HttpsConfigurator {
+        public Configurator(SSLContext ctx) {
+            super(ctx);
+        }
+
+        public void configure(HttpsParameters params) {
+            params.setSSLParameters(getSSLContext().getSupportedSSLParameters());
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/MockServer.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2015, 2017, 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.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocket;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Iterator;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A cut-down Http/1 Server for testing various error situations
+ *
+ * use interrupt() to halt
+ */
+public class MockServer extends Thread implements Closeable {
+
+    ServerSocket ss;
+    private final List<Connection> sockets;
+    private final List<Connection> removals;
+    private final List<Connection> additions;
+    AtomicInteger counter = new AtomicInteger(0);
+
+    // waits up to 20 seconds for something to happen
+    // dont use this unless certain activity coming.
+    public Connection activity() {
+        for (int i = 0; i < 80 * 100; i++) {
+            doRemovalsAndAdditions();
+            for (Connection c : sockets) {
+                if (c.poll()) {
+                    return c;
+                }
+            }
+            try {
+                Thread.sleep(250);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    private void doRemovalsAndAdditions() {
+        synchronized (removals) {
+            Iterator<Connection> i = removals.iterator();
+            while (i.hasNext()) {
+                Connection c = i.next();
+                System.out.println("socket removed: " + c);
+                sockets.remove(c);
+            }
+            removals.clear();
+        }
+
+        synchronized (additions) {
+            Iterator<Connection> i = additions.iterator();
+            while (i.hasNext()) {
+                Connection c = i.next();
+                System.out.println("socket added: " + c);
+                sockets.add(c);
+            }
+            additions.clear();
+        }
+    }
+
+    // clears all current connections on Server.
+    public void reset() {
+        for (Connection c : sockets) {
+            c.close();
+        }
+    }
+
+    /**
+     * Reads data into an ArrayBlockingQueue<String> where each String
+     * is a line of input, that was terminated by CRLF (not included)
+     */
+    class Connection extends Thread {
+        Connection(Socket s) throws IOException {
+            this.socket = s;
+            id = counter.incrementAndGet();
+            is = s.getInputStream();
+            os = s.getOutputStream();
+            incoming = new ArrayBlockingQueue<>(100);
+            setName("Server-Connection");
+            setDaemon(true);
+        }
+        final Socket socket;
+        final int id;
+        final InputStream is;
+        final OutputStream os;
+        final ArrayBlockingQueue<String> incoming;
+
+        final static String CRLF = "\r\n";
+
+        // sentinel indicating connection closed
+        final static String CLOSED = "C.L.O.S.E.D";
+        volatile boolean closed = false;
+        volatile boolean released = false;
+
+        @Override
+        public void run() {
+            byte[] buf = new byte[256];
+            String s = "";
+            try {
+                while (true) {
+                    int n = is.read(buf);
+                    if (n == -1) {
+                        cleanup();
+                        return;
+                    }
+                    String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
+                    s = s + s0;
+                    int i;
+                    while ((i=s.indexOf(CRLF)) != -1) {
+                        String s1 = s.substring(0, i+2);
+                        System.out.println("Server got: " + s1.substring(0,i));
+                        incoming.put(s1);
+                        if (i+2 == s.length()) {
+                            s = "";
+                            break;
+                        }
+                        s = s.substring(i+2);
+                    }
+                }
+            } catch (IOException |InterruptedException e1) {
+                cleanup();
+            } catch (Throwable t) {
+                System.out.println("X: " + t);
+                cleanup();
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "Server.Connection: " + socket.toString();
+        }
+
+        public void sendHttpResponse(int code, String body, String... headers)
+            throws IOException
+        {
+            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+            for (int i=0; i<headers.length; i+=2) {
+                r1 += headers[i] + ": " + headers[i+1] + CRLF;
+            }
+            int clen = body == null ? 0 : body.length();
+            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
+            r1 += CRLF;
+            if (body != null) {
+                r1 += body;
+            }
+            send(r1);
+        }
+
+        // content-length is 10 bytes too many
+        public void sendIncompleteHttpResponseBody(int code) throws IOException {
+            String body = "Hello World Helloworld Goodbye World";
+            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+            int clen = body.length() + 10;
+            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
+            r1 += CRLF;
+            if (body != null) {
+                r1 += body;
+            }
+            send(r1);
+        }
+
+        public void sendIncompleteHttpResponseHeaders(int code)
+            throws IOException
+        {
+            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
+            send(r1);
+        }
+
+        public void send(String r) throws IOException {
+            os.write(r.getBytes(StandardCharsets.ISO_8859_1));
+        }
+
+        public synchronized void close() {
+            cleanup();
+            closed = true;
+            incoming.clear();
+        }
+
+        public String nextInput(long timeout, TimeUnit unit) {
+            String result = "";
+            while (poll()) {
+                try {
+                    String s = incoming.poll(timeout, unit);
+                    if (s == null && closed) {
+                        return CLOSED;
+                    } else {
+                        result += s;
+                    }
+                } catch (InterruptedException e) {
+                    return null;
+                }
+            }
+            return result;
+        }
+
+        public String nextInput() {
+            return nextInput(0, TimeUnit.SECONDS);
+        }
+
+        public boolean poll() {
+            return incoming.peek() != null;
+        }
+
+        private void cleanup() {
+            if (released) return;
+            synchronized(this) {
+                if (released) return;
+                released = true;
+            }
+            try {
+                socket.close();
+            } catch (IOException e) {}
+            synchronized (removals) {
+                removals.add(this);
+            }
+        }
+    }
+
+    MockServer(int port, ServerSocketFactory factory) throws IOException {
+        ss = factory.createServerSocket(port);
+        sockets = Collections.synchronizedList(new LinkedList<>());
+        removals = new LinkedList<>();
+        additions = new LinkedList<>();
+        setName("Test-Server");
+        setDaemon(true);
+    }
+
+    MockServer(int port) throws IOException {
+        this(port, ServerSocketFactory.getDefault());
+    }
+
+    MockServer() throws IOException {
+        this(0);
+    }
+
+    int port() {
+        return ss.getLocalPort();
+    }
+
+    public String getURL() {
+        if (ss instanceof SSLServerSocket) {
+            return "https://127.0.0.1:" + port() + "/foo/";
+        } else {
+            return "http://127.0.0.1:" + port() + "/foo/";
+        }
+    }
+
+    private volatile boolean closed;
+
+    @Override
+    public void close() {
+        closed = true;
+        try {
+            ss.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        for (Connection c : sockets) {
+            c.close();
+        }
+    }
+
+    @Override
+    public void run() {
+        while (!closed) {
+            try {
+                System.out.println("Server waiting for connection");
+                Socket s = ss.accept();
+                Connection c = new Connection(s);
+                c.start();
+                System.out.println("Server got new connection: " + c);
+                synchronized (additions) {
+                    additions.add(c);
+                }
+            } catch (IOException e) {
+                if (closed)
+                    return;
+                e.printStackTrace();
+            }
+        }
+    }
+
+}
--- a/test/jdk/java/net/httpclient/MultiAuthTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/MultiAuthTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -46,7 +46,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import static java.nio.charset.StandardCharsets.US_ASCII;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
--- a/test/jdk/java/net/httpclient/RequestBodyTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/RequestBodyTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -22,7 +22,8 @@
  */
 
 /*
- * @test @bug 8087112
+ * @test
+ * @bug 8087112
  * @modules jdk.incubator.httpclient
  *          java.logging
  *          jdk.httpserver
@@ -42,6 +43,7 @@
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
 import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -51,14 +53,15 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 import javax.net.ssl.SSLContext;
 import jdk.test.lib.util.FileUtils;
+import static java.lang.System.out;
 import static java.nio.charset.StandardCharsets.*;
 import static java.nio.file.StandardOpenOption.*;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.*;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.*;
 import static jdk.incubator.http.HttpResponse.BodyHandler.*;
 
 import org.testng.annotations.AfterTest;
@@ -69,12 +72,12 @@
 
 public class RequestBodyTest {
 
-    static final String fileroot = System.getProperty("test.src") + "/docs";
+    static final String fileroot = System.getProperty("test.src", ".") + "/docs";
     static final String midSizedFilename = "/files/notsobigfile.txt";
     static final String smallFilename = "/files/smallfile.txt";
+    final ConcurrentHashMap<String,Throwable> failures = new ConcurrentHashMap<>();
 
     HttpClient client;
-    ExecutorService exec = Executors.newCachedThreadPool();
     String httpURI;
     String httpsURI;
 
@@ -109,14 +112,25 @@
                            .sslContext(ctx)
                            .version(HttpClient.Version.HTTP_1_1)
                            .followRedirects(HttpClient.Redirect.ALWAYS)
-                           .executor(exec)
                            .build();
     }
 
     @AfterTest
     public void teardown() throws Exception {
-        exec.shutdownNow();
-        LightWeightHttpServer.stop();
+        try {
+            LightWeightHttpServer.stop();
+        } finally {
+            System.out.println("RequestBodyTest: " + failures.size() + " failures");
+            int i = 0;
+            for (String key: failures.keySet()) {
+                System.out.println("test" + key + " failed: " + failures.get(key));
+                failures.get(key).printStackTrace(System.out);
+                if (i++ > 3) {
+                   System.out.println("..... other failures not printed ....");
+                   break;
+                }
+            }
+        }
     }
 
     @DataProvider
@@ -128,8 +142,9 @@
                 for (String file : new String[] { smallFilename, midSizedFilename })
                     for (RequestBody requestBodyType : RequestBody.values())
                         for (ResponseBody responseBodyType : ResponseBody.values())
-                            values.add(new Object[]
-                                {uri, requestBodyType, responseBodyType, file, async});
+                            for (boolean bufferResponseBody : new boolean[] { false, true })
+                                values.add(new Object[]
+                                    {uri, requestBodyType, responseBodyType, file, async, bufferResponseBody});
 
         return values.stream().toArray(Object[][]::new);
     }
@@ -139,15 +154,25 @@
                   RequestBody requestBodyType,
                   ResponseBody responseBodyType,
                   String file,
-                  boolean async)
+                  boolean async,
+                  boolean bufferResponseBody)
         throws Exception
     {
-        Path filePath = Paths.get(fileroot + file);
-        URI uri = new URI(target);
+        try {
+            Path filePath = Paths.get(fileroot + file);
+            URI uri = new URI(target);
+
+            HttpRequest request = createRequest(uri, requestBodyType, filePath);
 
-        HttpRequest request = createRequest(uri, requestBodyType, filePath);
-
-        checkResponse(client, request, requestBodyType, responseBodyType, filePath, async);
+            checkResponse(client, request, requestBodyType, responseBodyType, filePath, async, bufferResponseBody);
+        } catch (Exception | Error x) {
+            Object[] params = new Object[] {
+                target, requestBodyType, responseBodyType,
+                file, "async=" + async, "buffer=" + bufferResponseBody
+            };
+            failures.put(java.util.Arrays.toString(params), x);
+            throw x;
+        }
     }
 
     static final int DEFAULT_OFFSET = 10;
@@ -198,7 +223,8 @@
                        RequestBody requestBodyType,
                        ResponseBody responseBodyType,
                        Path file,
-                       boolean async)
+                       boolean async,
+                       boolean bufferResponseBody)
         throws InterruptedException, IOException
     {
         String filename = file.toFile().getAbsolutePath();
@@ -215,43 +241,57 @@
 
         switch (responseBodyType) {
             case BYTE_ARRAY:
-                HttpResponse<byte[]> bar = getResponse(client, request, asByteArray(), async);
+                BodyHandler<byte[]> bh = asByteArray();
+                if (bufferResponseBody) bh = buffering(bh, 50);
+                HttpResponse<byte[]> bar = getResponse(client, request, bh, async);
                 assertEquals(bar.statusCode(), 200);
                 assertEquals(bar.body(), fileAsBytes);
                 break;
             case BYTE_ARRAY_CONSUMER:
                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                HttpResponse<Void> v = getResponse(client, request,
-                        asByteArrayConsumer(o -> consumerBytes(o, baos) ), async);
+                Consumer<Optional<byte[]>> consumer = o -> consumerBytes(o, baos);
+                BodyHandler<Void> bh1 = asByteArrayConsumer(consumer);
+                if (bufferResponseBody) bh1 = buffering(bh1, 49);
+                HttpResponse<Void> v = getResponse(client, request, bh1, async);
                 byte[] ba = baos.toByteArray();
                 assertEquals(v.statusCode(), 200);
                 assertEquals(ba, fileAsBytes);
                 break;
             case DISCARD:
                 Object o = new Object();
-                HttpResponse<Object> or = getResponse(client, request, discard(o), async);
+                BodyHandler<Object> bh2 = discard(o);
+                if (bufferResponseBody) bh2 = buffering(bh2, 51);
+                HttpResponse<Object> or = getResponse(client, request, bh2, async);
                 assertEquals(or.statusCode(), 200);
                 assertSame(or.body(), o);
                 break;
             case FILE:
-                HttpResponse<Path> fr = getResponse(client, request, asFile(tempFile), async);
+                BodyHandler<Path> bh3 = asFile(tempFile);
+                if (bufferResponseBody) bh3 = buffering(bh3, 48);
+                HttpResponse<Path> fr = getResponse(client, request, bh3, async);
                 assertEquals(fr.statusCode(), 200);
                 assertEquals(Files.size(tempFile), fileAsString.length());
                 assertEquals(Files.readAllBytes(tempFile), fileAsBytes);
                 break;
             case FILE_WITH_OPTION:
-                fr = getResponse(client, request, asFile(tempFile, CREATE_NEW, WRITE), async);
+                BodyHandler<Path> bh4 = asFile(tempFile, CREATE_NEW, WRITE);
+                if (bufferResponseBody) bh4 = buffering(bh4, 52);
+                fr = getResponse(client, request, bh4, async);
                 assertEquals(fr.statusCode(), 200);
                 assertEquals(Files.size(tempFile), fileAsString.length());
                 assertEquals(Files.readAllBytes(tempFile), fileAsBytes);
                 break;
             case STRING:
-                HttpResponse<String> sr = getResponse(client, request, asString(), async);
+                BodyHandler<String> bh5 = asString();
+                if(bufferResponseBody) bh5 = buffering(bh5, 47);
+                HttpResponse<String> sr = getResponse(client, request, bh5, async);
                 assertEquals(sr.statusCode(), 200);
                 assertEquals(sr.body(), fileAsString);
                 break;
             case STRING_WITH_CHARSET:
-                HttpResponse<String> r = getResponse(client, request, asString(StandardCharsets.UTF_8), async);
+                BodyHandler<String> bh6 = asString(StandardCharsets.UTF_8);
+                if (bufferResponseBody) bh6 = buffering(bh6, 53);
+                HttpResponse<String> r = getResponse(client, request, bh6, async);
                 assertEquals(r.statusCode(), 200);
                 assertEquals(r.body(), fileAsString);
                 break;
@@ -303,4 +343,28 @@
             throw new UncheckedIOException(x);
         }
     }
+
+    // ---
+
+    /* Main entry point for standalone testing of the main functional test. */
+    public static void main(String... args) throws Exception {
+        RequestBodyTest t = new RequestBodyTest();
+        t.setup();
+        int count = 0;
+        try {
+            for (Object[] objs : t.exchanges()) {
+                count++;
+                out.printf("********* iteration: %d %s %s %s %s %s %s *********%n",
+                           count, objs[0], objs[1], objs[2], objs[3], objs[4], objs[5]);
+                t.exchange((String) objs[0],
+                           (RequestBody) objs[1],
+                           (ResponseBody) objs[2],
+                           (String) objs[3],
+                           (boolean) objs[4],
+                           (boolean) objs[5]);
+            }
+        } finally {
+            t.teardown();
+        }
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,341 @@
+/*
+ * Copyright (c) 2017, 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 HttpRequest[.Builder] API and behaviour checks
+ * @run testng RequestBuilderTest
+ */
+
+import java.net.URI;
+import java.util.List;
+import jdk.incubator.http.HttpRequest;
+import static java.time.Duration.ofNanos;
+import static java.time.Duration.ofMinutes;
+import static java.time.Duration.ofSeconds;
+import static java.time.Duration.ZERO;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
+import static jdk.incubator.http.HttpRequest.newBuilder;
+import static org.testng.Assert.*;
+import org.testng.annotations.Test;
+
+public class RequestBuilderTest {
+
+    static final URI uri = URI.create("http://foo.com/");
+    static final Class<NullPointerException> NPE = NullPointerException.class;
+    static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
+    static final Class<IllegalStateException> ISE = IllegalStateException.class;
+    static final Class<NumberFormatException> NFE = NumberFormatException.class;
+    static final Class<UnsupportedOperationException> UOE = UnsupportedOperationException.class;
+
+    @Test
+    public void testDefaults() {
+        List<HttpRequest.Builder> builders = List.of(newBuilder().uri(uri),
+                                                     newBuilder(uri));
+        for (HttpRequest.Builder builder : builders) {
+            assertFalse(builder.build().expectContinue());
+            assertEquals(builder.build().method(), "GET");
+            assertFalse(builder.build().bodyPublisher().isPresent());
+            assertFalse(builder.build().version().isPresent());
+            assertFalse(builder.build().timeout().isPresent());
+            assertTrue(builder.build().headers() != null);
+            assertEquals(builder.build().headers().map().size(), 0);
+        }
+    }
+
+    @Test
+    public void testNull() {
+        HttpRequest.Builder builder = newBuilder();
+
+        assertThrows(NPE, () -> newBuilder(null).build());
+        assertThrows(NPE, () -> newBuilder(uri).uri(null).build());
+        assertThrows(NPE, () -> builder.uri(null));
+        assertThrows(NPE, () -> builder.version(null));
+        assertThrows(NPE, () -> builder.header(null, null));
+        assertThrows(NPE, () -> builder.header("name", null));
+        assertThrows(NPE, () -> builder.header(null, "value"));
+        assertThrows(NPE, () -> builder.headers(null));
+        assertThrows(NPE, () -> builder.headers(new String[] { null, null }));
+        assertThrows(NPE, () -> builder.headers(new String[] { "name", null }));
+        assertThrows(NPE, () -> builder.headers(new String[] { null, "value" }));
+        assertThrows(NPE, () -> builder.method(null, null));
+        assertThrows(NPE, () -> builder.method("GET", null));
+        assertThrows(NPE, () -> builder.method("POST", null));
+        assertThrows(NPE, () -> builder.method("PUT", null));
+        assertThrows(NPE, () -> builder.method("DELETE", null));
+        assertThrows(NPE, () -> builder.setHeader(null, null));
+        assertThrows(NPE, () -> builder.setHeader("name", null));
+        assertThrows(NPE, () -> builder.setHeader(null, "value"));
+        assertThrows(NPE, () -> builder.timeout(null));
+        assertThrows(NPE, () -> builder.DELETE(null));
+        assertThrows(NPE, () -> builder.POST(null));
+        assertThrows(NPE, () -> builder.PUT(null));
+    }
+
+    @Test
+    public void testURI() {
+        assertThrows(ISE, () -> newBuilder().build());
+        List<URI> uris = List.of(
+                URI.create("ws://foo.com"),
+                URI.create("wss://foo.com"),
+                URI.create("ftp://foo.com"),
+                URI.create("gopher://foo.com"),
+                URI.create("mailto:a@b.com"),
+                URI.create("scheme:example.com"),
+                URI.create("scheme:example.com"),
+                URI.create("scheme:example.com/path"),
+                URI.create("path"),
+                URI.create("/path")
+        );
+        for (URI u : uris) {
+            assertThrows(IAE, () -> newBuilder(u));
+            assertThrows(IAE, () -> newBuilder().uri(u));
+        }
+
+        assertEquals(newBuilder(uri).build().uri(), uri);
+        assertEquals(newBuilder().uri(uri).build().uri(), uri);
+        URI https = URI.create("https://foo.com");
+        assertEquals(newBuilder(https).build().uri(), https);
+        assertEquals(newBuilder().uri(https).build().uri(), https);
+    }
+
+    @Test
+    public void testMethod() {
+        HttpRequest request = newBuilder(uri).build();
+        assertEquals(request.method(), "GET");
+        assertTrue(!request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).GET().build();
+        assertEquals(request.method(), "GET");
+        assertTrue(!request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).POST(fromString("")).GET().build();
+        assertEquals(request.method(), "GET");
+        assertTrue(!request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).PUT(fromString("")).GET().build();
+        assertEquals(request.method(), "GET");
+        assertTrue(!request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).DELETE(fromString("")).GET().build();
+        assertEquals(request.method(), "GET");
+        assertTrue(!request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).POST(fromString("")).build();
+        assertEquals(request.method(), "POST");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).PUT(fromString("")).build();
+        assertEquals(request.method(), "PUT");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).DELETE(fromString("")).build();
+        assertEquals(request.method(), "DELETE");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).GET().POST(fromString("")).build();
+        assertEquals(request.method(), "POST");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).GET().PUT(fromString("")).build();
+        assertEquals(request.method(), "PUT");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).GET().DELETE(fromString("")).build();
+        assertEquals(request.method(), "DELETE");
+        assertTrue(request.bodyPublisher().isPresent());
+    }
+
+    @Test
+    public void testHeaders() {
+        HttpRequest.Builder builder = newBuilder(uri);
+
+        String[] empty = new String[0];
+        assertThrows(IAE, () -> builder.headers(empty).build());
+        assertThrows(IAE, () -> builder.headers("1").build());
+        assertThrows(IAE, () -> builder.headers("1", "2", "3").build());
+        assertThrows(IAE, () -> builder.headers("1", "2", "3", "4", "5").build());
+        assertEquals(builder.build().headers().map().size(),0);
+
+        List<HttpRequest> requests = List.of(
+                // same header built from different combinations of the API
+                newBuilder(uri).header("A", "B").build(),
+                newBuilder(uri).headers("A", "B").build(),
+                newBuilder(uri).setHeader("A", "B").build(),
+                newBuilder(uri).header("A", "F").setHeader("A", "B").build(),
+                newBuilder(uri).headers("A", "F").setHeader("A", "B").build()
+        );
+
+        for (HttpRequest r : requests) {
+            assertEquals(r.headers().map().size(), 1);
+            assertTrue(r.headers().firstValue("A").isPresent());
+            assertEquals(r.headers().firstValue("A").get(), "B");
+            assertEquals(r.headers().allValues("A"), List.of("B"));
+            assertEquals(r.headers().allValues("C").size(), 0);
+            assertEquals(r.headers().map().get("A"), List.of("B"));
+            assertThrows(NFE, () -> r.headers().firstValueAsLong("A"));
+            assertFalse(r.headers().firstValue("C").isPresent());
+            // a non-exhaustive list of mutators
+            assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+            assertThrows(UOE, () -> r.headers().map().remove("A"));
+            assertThrows(UOE, () -> r.headers().map().remove("A", "B"));
+            assertThrows(UOE, () -> r.headers().map().clear());
+            assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+            assertThrows(UOE, () -> r.headers().allValues("A").remove(1));
+            assertThrows(UOE, () -> r.headers().allValues("A").clear());
+            assertThrows(UOE, () -> r.headers().allValues("A").add("Z"));
+            assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
+            assertThrows(UOE, () -> r.headers().allValues("A").add(1, "Z"));
+        }
+
+        requests = List.of(
+                // same headers built from different combinations of the API
+                newBuilder(uri).header("A", "B")
+                               .header("C", "D").build(),
+                newBuilder(uri).header("A", "B")
+                               .headers("C", "D").build(),
+                newBuilder(uri).header("A", "B")
+                               .setHeader("C", "D").build(),
+                newBuilder(uri).headers("A", "B")
+                               .headers("C", "D").build(),
+                newBuilder(uri).headers("A", "B")
+                               .header("C", "D").build(),
+                newBuilder(uri).headers("A", "B")
+                               .setHeader("C", "D").build(),
+                newBuilder(uri).setHeader("A", "B")
+                               .setHeader("C", "D").build(),
+                newBuilder(uri).setHeader("A", "B")
+                               .header("C", "D").build(),
+                newBuilder(uri).setHeader("A", "B")
+                               .headers("C", "D").build(),
+                newBuilder(uri).headers("A", "B", "C", "D").build()
+        );
+
+        for (HttpRequest r : requests) {
+            assertEquals(r.headers().map().size(), 2);
+            assertTrue(r.headers().firstValue("A").isPresent());
+            assertEquals(r.headers().firstValue("A").get(), "B");
+            assertEquals(r.headers().allValues("A"), List.of("B"));
+            assertTrue(r.headers().firstValue("C").isPresent());
+            assertEquals(r.headers().firstValue("C").get(), "D");
+            assertEquals(r.headers().allValues("C"), List.of("D"));
+            assertEquals(r.headers().map().get("C"), List.of("D"));
+            assertThrows(NFE, () -> r.headers().firstValueAsLong("C"));
+            assertFalse(r.headers().firstValue("E").isPresent());
+            // a smaller non-exhaustive list of mutators
+            assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+            assertThrows(UOE, () -> r.headers().map().remove("C"));
+            assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+            assertThrows(UOE, () -> r.headers().allValues("A").clear());
+            assertThrows(UOE, () -> r.headers().allValues("C").add("Z"));
+        }
+
+        requests = List.of(
+                // same multi-value headers built from different combinations of the API
+                newBuilder(uri).header("A", "B")
+                               .header("A", "C").build(),
+                newBuilder(uri).header("A", "B")
+                               .headers("A", "C").build(),
+                newBuilder(uri).headers("A", "B")
+                               .headers("A", "C").build(),
+                newBuilder(uri).headers("A", "B")
+                               .header("A", "C").build(),
+                newBuilder(uri).setHeader("A", "B")
+                               .header("A", "C").build(),
+                newBuilder(uri).setHeader("A", "B")
+                               .headers("A", "C").build(),
+                newBuilder(uri).header("A", "D")
+                               .setHeader("A", "B")
+                               .headers("A", "C").build(),
+                newBuilder(uri).headers("A", "B", "A", "C").build()
+        );
+
+        for (HttpRequest r : requests) {
+            assertEquals(r.headers().map().size(), 1);
+            assertTrue(r.headers().firstValue("A").isPresent());
+            assertTrue(r.headers().allValues("A").containsAll(List.of("B", "C")));
+            assertEquals(r.headers().allValues("C").size(), 0);
+            assertEquals(r.headers().map().get("A"), List.of("B", "C"));
+            assertThrows(NFE, () -> r.headers().firstValueAsLong("A"));
+            assertFalse(r.headers().firstValue("C").isPresent());
+            // a non-exhaustive list of mutators
+            assertThrows(UOE, () -> r.headers().map().put("Z", List.of("Z")));
+            assertThrows(UOE, () -> r.headers().map().remove("A"));
+            assertThrows(UOE, () -> r.headers().map().remove("A", "B"));
+            assertThrows(UOE, () -> r.headers().map().clear());
+            assertThrows(UOE, () -> r.headers().allValues("A").remove("B"));
+            assertThrows(UOE, () -> r.headers().allValues("A").remove(1));
+            assertThrows(UOE, () -> r.headers().allValues("A").clear());
+            assertThrows(UOE, () -> r.headers().allValues("A").add("Z"));
+            assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
+            assertThrows(UOE, () -> r.headers().allValues("A").add(1, "Z"));
+        }
+    }
+
+    @Test
+    public void testCopy() {
+        HttpRequest.Builder builder = newBuilder(uri).expectContinue(true)
+                                                     .header("A", "B")
+                                                     .POST(fromString(""))
+                                                     .timeout(ofSeconds(30))
+                                                     .version(HTTP_1_1);
+        HttpRequest.Builder copy = builder.copy();
+        assertTrue(builder != copy);
+
+        // modify the original builder before building from the copy
+        builder.GET().timeout(ofSeconds(5)).version(HTTP_2).setHeader("A", "C");
+
+        HttpRequest copyRequest = copy.build();
+        assertEquals(copyRequest.uri(), uri);
+        assertEquals(copyRequest.expectContinue(), true);
+        assertEquals(copyRequest.headers().map().get("A"), List.of("B"));
+        assertEquals(copyRequest.method(), "POST");
+        assertEquals(copyRequest.bodyPublisher().isPresent(), true);
+        assertEquals(copyRequest.timeout().get(), ofSeconds(30));
+        assertTrue(copyRequest.version().isPresent());
+        assertEquals(copyRequest.version().get(), HTTP_1_1);
+    }
+
+    @Test
+    public void testTimeout() {
+        HttpRequest.Builder builder = newBuilder(uri);
+        assertThrows(IAE, () -> builder.timeout(ZERO));
+        assertThrows(IAE, () -> builder.timeout(ofSeconds(0)));
+        assertThrows(IAE, () -> builder.timeout(ofSeconds(-1)));
+        assertThrows(IAE, () -> builder.timeout(ofNanos(-100)));
+        assertEquals(builder.timeout(ofNanos(15)).build().timeout().get(), ofNanos(15));
+        assertEquals(builder.timeout(ofSeconds(50)).build().timeout().get(), ofSeconds(50));
+        assertEquals(builder.timeout(ofMinutes(30)).build().timeout().get(), ofMinutes(30));
+    }
+
+    @Test
+    public void testExpect() {
+        HttpRequest.Builder builder = newBuilder(uri);
+        assertEquals(builder.build().expectContinue(), false);
+        assertEquals(builder.expectContinue(true).build().expectContinue(), true);
+        assertEquals(builder.expectContinue(false).build().expectContinue(), false);
+        assertEquals(builder.expectContinue(true).build().expectContinue(), true);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/RequestProcessorExceptions.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2017, 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
+ * @run testng RequestProcessorExceptions
+ */
+
+import java.io.FileNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromByteArray;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+
+public class RequestProcessorExceptions {
+
+    @DataProvider(name = "byteArrayOOBs")
+    public Object[][] byteArrayOOBs() {
+        return new Object[][] {
+                { new byte[100],    1,  100 },
+                { new byte[100],   -1,   10 },
+                { new byte[100],   99,    2 },
+                { new byte[1],   -100,    1 } };
+    }
+
+    @Test(dataProvider = "byteArrayOOBs", expectedExceptions = IndexOutOfBoundsException.class)
+    public void fromByteArrayCheck(byte[] buf, int offset, int length) {
+        fromByteArray(buf, offset, length);
+    }
+
+    @DataProvider(name = "nonExistentFiles")
+    public Object[][] nonExistentFiles() {
+        List<Path> paths = List.of(Paths.get("doesNotExist"),
+                                   Paths.get("tsixEtoNseod"),
+                                   Paths.get("doesNotExist2"));
+        paths.forEach(p -> {
+            if (Files.exists(p))
+                throw new AssertionError("Unexpected " + p);
+        });
+
+        return paths.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "nonExistentFiles", expectedExceptions = FileNotFoundException.class)
+    public void fromFileCheck(Path path) throws Exception {
+        fromFile(path);
+    }
+
+    // ---
+
+    /* Main entry point for standalone testing of the main functional test. */
+    public static void main(String... args) throws Exception {
+        RequestProcessorExceptions t = new RequestProcessorExceptions();
+        for (Object[] objs : t.byteArrayOOBs()) {
+            try {
+                t.fromByteArrayCheck((byte[]) objs[0], (int) objs[1], (int) objs[2]);
+                throw new RuntimeException("fromByteArrayCheck failed");
+            } catch (IndexOutOfBoundsException expected) { /* Ok */ }
+        }
+        for (Object[] objs : t.nonExistentFiles()) {
+            try {
+                t.fromFileCheck((Path) objs[0]);
+                throw new RuntimeException("fromFileCheck failed");
+            } catch (FileNotFoundException expected) { /* Ok */ }
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/Server.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,286 +0,0 @@
-/*
- * Copyright (c) 2015, 2016, 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.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Iterator;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * A cut-down Http/1 Server for testing various error situations
- *
- * use interrupt() to halt
- */
-public class Server extends Thread implements Closeable {
-
-    ServerSocket ss;
-    private final List<Connection> sockets;
-    private final List<Connection> removals;
-    private final List<Connection> additions;
-    AtomicInteger counter = new AtomicInteger(0);
-
-    // waits up to 20 seconds for something to happen
-    // dont use this unless certain activity coming.
-    public Connection activity() {
-        for (int i = 0; i < 80 * 100; i++) {
-            doRemovalsAndAdditions();
-            for (Connection c : sockets) {
-                if (c.poll()) {
-                    return c;
-                }
-            }
-            try {
-                Thread.sleep(250);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
-        return null;
-    }
-
-    private void doRemovalsAndAdditions() {
-        if (removals.isEmpty() && additions.isEmpty())
-            return;
-        Iterator<Connection> i = removals.iterator();
-        while (i.hasNext())
-            sockets.remove(i.next());
-        removals.clear();
-
-        i = additions.iterator();
-        while (i.hasNext())
-            sockets.add(i.next());
-        additions.clear();
-    }
-
-    // clears all current connections on Server.
-    public void reset() {
-        for (Connection c : sockets) {
-            c.close();
-        }
-    }
-
-    /**
-     * Reads data into an ArrayBlockingQueue<String> where each String
-     * is a line of input, that was terminated by CRLF (not included)
-     */
-    class Connection extends Thread {
-        Connection(Socket s) throws IOException {
-            this.socket = s;
-            id = counter.incrementAndGet();
-            is = s.getInputStream();
-            os = s.getOutputStream();
-            incoming = new ArrayBlockingQueue<>(100);
-            setName("Server-Connection");
-            setDaemon(true);
-        }
-        final Socket socket;
-        final int id;
-        final InputStream is;
-        final OutputStream os;
-        final ArrayBlockingQueue<String> incoming;
-
-        final static String CRLF = "\r\n";
-
-        // sentinel indicating connection closed
-        final static String CLOSED = "C.L.O.S.E.D";
-        volatile boolean closed = false;
-
-        @Override
-        public void run() {
-            byte[] buf = new byte[256];
-            String s = "";
-            try {
-                while (true) {
-                    int n = is.read(buf);
-                    if (n == -1) {
-                        cleanup();
-                        return;
-                    }
-                    String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
-                    s = s + s0;
-                    int i;
-                    while ((i=s.indexOf(CRLF)) != -1) {
-                        String s1 = s.substring(0, i+2);
-                        incoming.put(s1);
-                        if (i+2 == s.length()) {
-                            s = "";
-                            break;
-                        }
-                        s = s.substring(i+2);
-                    }
-                }
-            } catch (IOException |InterruptedException e1) {
-                cleanup();
-            } catch (Throwable t) {
-                System.out.println("X: " + t);
-                cleanup();
-            }
-        }
-
-        @Override
-        public String toString() {
-            return "Server.Connection: " + socket.toString();
-        }
-
-        public void sendHttpResponse(int code, String body, String... headers)
-            throws IOException
-        {
-            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
-            for (int i=0; i<headers.length; i+=2) {
-                r1 += headers[i] + ": " + headers[i+1] + CRLF;
-            }
-            int clen = body == null ? 0 : body.length();
-            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
-            r1 += CRLF;
-            if (body != null) {
-                r1 += body;
-            }
-            send(r1);
-        }
-
-        // content-length is 10 bytes too many
-        public void sendIncompleteHttpResponseBody(int code) throws IOException {
-            String body = "Hello World Helloworld Goodbye World";
-            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
-            int clen = body.length() + 10;
-            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
-            r1 += CRLF;
-            if (body != null) {
-                r1 += body;
-            }
-            send(r1);
-        }
-
-        public void sendIncompleteHttpResponseHeaders(int code)
-            throws IOException
-        {
-            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
-            send(r1);
-        }
-
-        public void send(String r) throws IOException {
-            os.write(r.getBytes(StandardCharsets.ISO_8859_1));
-        }
-
-        public synchronized void close() {
-            cleanup();
-            closed = true;
-            incoming.clear();
-        }
-
-        public String nextInput(long timeout, TimeUnit unit) {
-            String result = "";
-            while (poll()) {
-                try {
-                    String s = incoming.poll(timeout, unit);
-                    if (s == null && closed) {
-                        return CLOSED;
-                    } else {
-                        result += s;
-                    }
-                } catch (InterruptedException e) {
-                    return null;
-                }
-            }
-            return result;
-        }
-
-        public String nextInput() {
-            return nextInput(0, TimeUnit.SECONDS);
-        }
-
-        public boolean poll() {
-            return incoming.peek() != null;
-        }
-
-        private void cleanup() {
-            try {
-                socket.close();
-            } catch (IOException e) {}
-            removals.add(this);
-        }
-    }
-
-    Server(int port) throws IOException {
-        ss = new ServerSocket(port);
-        sockets = Collections.synchronizedList(new LinkedList<>());
-        removals = Collections.synchronizedList(new LinkedList<>());
-        additions = Collections.synchronizedList(new LinkedList<>());
-        setName("Test-Server");
-        setDaemon(true);
-    }
-
-    Server() throws IOException {
-        this(0);
-    }
-
-    int port() {
-        return ss.getLocalPort();
-    }
-
-    public String getURL() {
-        return "http://127.0.0.1:" + port() + "/foo/";
-    }
-
-    private volatile boolean closed;
-
-    @Override
-    public void close() {
-        closed = true;
-        try {
-            ss.close();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-        for (Connection c : sockets) {
-            c.close();
-        }
-    }
-
-    @Override
-    public void run() {
-        while (!closed) {
-            try {
-                Socket s = ss.accept();
-                Connection c = new Connection(s);
-                c.start();
-                additions.add(c);
-            } catch (IOException e) {
-                if (closed)
-                    return;
-                e.printStackTrace();
-            }
-        }
-    }
-
-}
--- a/test/jdk/java/net/httpclient/ShortRequestBody.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/ShortRequestBody.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -21,10 +21,10 @@
  * questions.
  */
 
-import java.io.*;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpResponse;
-import jdk.incubator.http.HttpRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.URI;
@@ -32,14 +32,20 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Flow;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
-import static java.lang.System.out;
+import java.util.function.Supplier;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpTimeoutException;
+
+import static java.lang.System.err;
 import static java.nio.charset.StandardCharsets.US_ASCII;
 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -48,23 +54,13 @@
  * @test
  * @bug 8151441
  * @summary Request body of incorrect (larger or smaller) sizes than that
- *          reported by the body processor
+ *          reported by the body publisher
  * @run main/othervm ShortRequestBody
  */
 
 public class ShortRequestBody {
 
     static final Path testSrc = Paths.get(System.getProperty("test.src", "."));
-    static volatile HttpClient staticDefaultClient;
-
-    static HttpClient defaultClient() {
-        if (staticDefaultClient == null) {
-            synchronized (ShortRequestBody.class) {
-                staticDefaultClient = HttpClient.newHttpClient();
-            }
-        }
-        return staticDefaultClient;
-    }
 
     // Some body types ( sources ) for testing.
     static final String STRING_BODY = "Hello world";
@@ -79,14 +75,14 @@
                                                   fileSize(FILE_BODY) };
     static final int[] BODY_OFFSETS = new int[] { 0, +1, -1, +2, -2, +3, -3 };
 
-    // A delegating body processor. Subtypes will have a concrete body type.
+    // A delegating Body Publisher. Subtypes will have a concrete body type.
     static abstract class AbstractDelegateRequestBody
-            implements HttpRequest.BodyProcessor {
+            implements HttpRequest.BodyPublisher {
 
-        final HttpRequest.BodyProcessor delegate;
+        final HttpRequest.BodyPublisher delegate;
         final long contentLength;
 
-        AbstractDelegateRequestBody(HttpRequest.BodyProcessor delegate,
+        AbstractDelegateRequestBody(HttpRequest.BodyPublisher delegate,
                                     long contentLength) {
             this.delegate = delegate;
             this.contentLength = contentLength;
@@ -101,26 +97,26 @@
         public long contentLength() { return contentLength; /* may be wrong! */ }
     }
 
-    // Request body processors that may generate a different number of actual
+    // Request body Publishers that may generate a different number of actual
     // bytes to that of what is reported through their {@code contentLength}.
 
     static class StringRequestBody extends AbstractDelegateRequestBody {
         StringRequestBody(String body, int additionalLength) {
-            super(HttpRequest.BodyProcessor.fromString(body),
+            super(HttpRequest.BodyPublisher.fromString(body),
                   body.getBytes(UTF_8).length + additionalLength);
         }
     }
 
     static class ByteArrayRequestBody extends AbstractDelegateRequestBody {
         ByteArrayRequestBody(byte[] body, int additionalLength) {
-            super(HttpRequest.BodyProcessor.fromByteArray(body),
+            super(HttpRequest.BodyPublisher.fromByteArray(body),
                   body.length + additionalLength);
         }
     }
 
     static class FileRequestBody extends AbstractDelegateRequestBody {
         FileRequestBody(Path path, int additionalLength) throws IOException {
-            super(HttpRequest.BodyProcessor.fromFile(path),
+            super(HttpRequest.BodyPublisher.fromFile(path),
                   Files.size(path) + additionalLength);
         }
     }
@@ -128,53 +124,63 @@
     // ---
 
     public static void main(String[] args) throws Exception {
-        try (Server server = new Server()) {
-            URI uri = new URI("http://127.0.0.1:" + server.getPort() + "/");
+        HttpClient sharedClient = HttpClient.newHttpClient();
+        List<Supplier<HttpClient>> clientSuppliers = new ArrayList<>();
+        clientSuppliers.add(() -> HttpClient.newHttpClient());
+        clientSuppliers.add(() -> sharedClient);
 
-            // sanity
-            success(uri, new StringRequestBody(STRING_BODY, 0));
-            success(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
-            success(uri, new FileRequestBody(FILE_BODY, 0));
+        try (Server server = new Server()) {
+            for (Supplier<HttpClient> cs : clientSuppliers) {
+                err.println("\n---- next supplier ----\n");
+                URI uri = new URI("http://127.0.0.1:" + server.getPort() + "/");
 
-            for (int i=1; i< BODY_OFFSETS.length; i++) {
-                failureBlocking(uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
-                failureBlocking(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
-                failureBlocking(uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+                // sanity ( 6 requests to keep client and server offsets easy to workout )
+                success(cs, uri, new StringRequestBody(STRING_BODY, 0));
+                success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
+                success(cs, uri, new FileRequestBody(FILE_BODY, 0));
+                success(cs, uri, new StringRequestBody(STRING_BODY, 0));
+                success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0));
+                success(cs, uri, new FileRequestBody(FILE_BODY, 0));
 
-                failureNonBlocking(uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
-                failureNonBlocking(uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
-                failureNonBlocking(uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
-            }
-        } finally {
-            Executor def = defaultClient().executor();
-            if (def instanceof ExecutorService) {
-               ((ExecutorService)def).shutdownNow();
+                for (int i = 1; i < BODY_OFFSETS.length; i++) {
+                    failureBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
+                    failureBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
+                    failureBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+
+                    failureNonBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i]));
+                    failureNonBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i]));
+                    failureNonBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i]));
+                }
             }
         }
     }
 
-    static void success(URI uri, HttpRequest.BodyProcessor processor)
+    static void success(Supplier<HttpClient> clientSupplier,
+                        URI uri,
+                        HttpRequest.BodyPublisher publisher)
         throws Exception
     {
         CompletableFuture<HttpResponse<Void>> cf;
         HttpRequest request = HttpRequest.newBuilder(uri)
-                                         .POST(processor)
+                                         .POST(publisher)
                                          .build();
-        cf = defaultClient().sendAsync(request, discard(null));
+        cf = clientSupplier.get().sendAsync(request, discard(null));
 
         HttpResponse<Void> resp = cf.get(30, TimeUnit.SECONDS);
-        out.println("Response code: " + resp.statusCode());
+        err.println("Response code: " + resp.statusCode());
         check(resp.statusCode() == 200, "Expected 200, got ", resp.statusCode());
     }
 
-    static void failureNonBlocking(URI uri, HttpRequest.BodyProcessor processor)
+    static void failureNonBlocking(Supplier<HttpClient> clientSupplier,
+                                   URI uri,
+                                   HttpRequest.BodyPublisher publisher)
         throws Exception
     {
         CompletableFuture<HttpResponse<Void>> cf;
         HttpRequest request = HttpRequest.newBuilder(uri)
-                                         .POST(processor)
+                                         .POST(publisher)
                                          .build();
-        cf = defaultClient().sendAsync(request, discard(null));
+        cf = clientSupplier.get().sendAsync(request, discard(null));
 
         try {
             HttpResponse<Void> r = cf.get(30, TimeUnit.SECONDS);
@@ -182,23 +188,34 @@
         } catch (TimeoutException x) {
             throw new RuntimeException("Unexpected timeout", x);
         } catch (ExecutionException expected) {
-            out.println("Caught expected: " + expected);
-            check(expected.getCause() instanceof IOException,
+            err.println("Caught expected: " + expected);
+            Throwable t = expected.getCause();
+            check(t instanceof IOException,
                   "Expected cause IOException, but got: ", expected.getCause());
+            String msg = t.getMessage();
+            check(msg.contains("Too many") || msg.contains("Too few"),
+                    "Expected Too many|Too few, got: ", t);
         }
     }
 
-    static void failureBlocking(URI uri, HttpRequest.BodyProcessor processor)
+    static void failureBlocking(Supplier<HttpClient> clientSupplier,
+                                URI uri,
+                                HttpRequest.BodyPublisher publisher)
         throws Exception
     {
         HttpRequest request = HttpRequest.newBuilder(uri)
-                                         .POST(processor)
+                                         .POST(publisher)
                                          .build();
         try {
-            HttpResponse<Void> r = defaultClient().send(request, discard(null));
+            HttpResponse<Void> r = clientSupplier.get().send(request, discard(null));
             throw new RuntimeException("Unexpected response: " + r.statusCode());
+        } catch (HttpTimeoutException x) {
+            throw new RuntimeException("Unexpected timeout", x);
         } catch (IOException expected) {
-            out.println("Caught expected: " + expected);
+            err.println("Caught expected: " + expected);
+            String msg = expected.getMessage();
+            check(msg.contains("Too many") || msg.contains("Too few"),
+                    "Expected Too many|Too few, got: ", expected);
         }
     }
 
@@ -225,20 +242,40 @@
 
             while (!closed) {
                 try (Socket s = ss.accept()) {
+                    err.println("Server: got connection");
                     InputStream is = s.getInputStream();
                     readRequestHeaders(is);
                     byte[] ba = new byte[1024];
 
                     int length = BODY_LENGTHS[count % 3];
                     length += BODY_OFFSETS[offset];
-
-                    is.readNBytes(ba, 0, length);
+                    err.println("Server: count=" + count + ", offset=" + offset);
+                    err.println("Server: expecting " +length+ " bytes");
+                    int read = is.readNBytes(ba, 0, length);
+                    err.println("Server: actually read " + read + " bytes");
 
-                    OutputStream os = s.getOutputStream();
-                    os.write(RESPONSE.getBytes(US_ASCII));
+                    // Update the counts before replying, to prevent the
+                    // client-side racing reset with this thread.
                     count++;
                     if (count % 6 == 0) // 6 is the number of failure requests per offset
                         offset++;
+                    if (count % 42 == 0) {
+                        count = 0;  // reset, for second iteration
+                        offset = 0;
+                    }
+
+                    if (read < length) {
+                        // no need to reply, client has already closed
+                        // ensure closed
+                        if (is.read() != -1)
+                            new AssertionError("Unexpected read");
+                    } else {
+                        OutputStream os = s.getOutputStream();
+                        err.println("Server: writing "
+                                + RESPONSE.getBytes(US_ASCII).length + " bytes");
+                        os.write(RESPONSE.getBytes(US_ASCII));
+                    }
+
                 } catch (IOException e) {
                     if (!closed)
                         System.out.println("Unexpected" + e);
--- a/test/jdk/java/net/httpclient/SmallTimeout.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SmallTimeout.java	Sun Nov 05 17:32:13 2017 +0000
@@ -40,7 +40,7 @@
  * @test
  * @bug 8178147
  * @summary Ensures that small timeouts do not cause hangs due to race conditions
- * @run main/othervm SmallTimeout
+ * @run main/othervm -Djdk.incubator.http.internal.common.DEBUG=true SmallTimeout
  */
 
 // To enable logging use. Not enabled by default as it changes the dynamics
@@ -52,7 +52,25 @@
     static int[] TIMEOUTS = {2, 1, 3, 2, 100, 1};
 
     // A queue for placing timed out requests so that their order can be checked.
-    static LinkedBlockingQueue<HttpRequest> queue = new LinkedBlockingQueue<>();
+    static LinkedBlockingQueue<HttpResult> queue = new LinkedBlockingQueue<>();
+
+    static final class HttpResult {
+         final HttpRequest request;
+         final Throwable   failed;
+         HttpResult(HttpRequest request, Throwable   failed) {
+             this.request = request;
+             this.failed = failed;
+         }
+
+         static HttpResult of(HttpRequest request) {
+             return new HttpResult(request, null);
+         }
+
+         static HttpResult of(HttpRequest request, Throwable t) {
+             return new HttpResult(request, t);
+         }
+
+    }
 
     static volatile boolean error;
 
@@ -76,8 +94,10 @@
                 CompletableFuture<HttpResponse<Object>> response = client
                     .sendAsync(req, discard(null))
                     .whenComplete((HttpResponse<Object> r, Throwable t) -> {
+                        Throwable cause = null;
                         if (r != null) {
                             out.println("Unexpected response: " + r);
+                            cause = new RuntimeException("Unexpected response");
                             error = true;
                         }
                         if (t != null) {
@@ -85,6 +105,7 @@
                                 out.println("Wrong exception type:" + t.toString());
                                 Throwable c = t.getCause() == null ? t : t.getCause();
                                 c.printStackTrace();
+                                cause = c;
                                 error = true;
                             } else {
                                 out.println("Caught expected timeout: " + t.getCause());
@@ -92,9 +113,10 @@
                         }
                         if (t == null && r == null) {
                             out.println("Both response and throwable are null!");
+                            cause = new RuntimeException("Both response and throwable are null!");
                             error = true;
                         }
-                        queue.add(req);
+                        queue.add(HttpResult.of(req,cause));
                     });
             }
             System.out.println("All requests submitted. Waiting ...");
@@ -118,15 +140,18 @@
 
                 final HttpRequest req = requests[i];
                 executor.execute(() -> {
+                    Throwable cause = null;
                     try {
                         client.send(req, discard(null));
                     } catch (HttpTimeoutException e) {
                         out.println("Caught expected timeout: " + e);
-                        queue.offer(req);
-                    } catch (IOException | InterruptedException ee) {
+                    } catch (Throwable ee) {
                         Throwable c = ee.getCause() == null ? ee : ee.getCause();
                         c.printStackTrace();
+                        cause = c;
                         error = true;
+                    } finally {
+                        queue.offer(HttpResult.of(req, cause));
                     }
                 });
             }
@@ -139,18 +164,20 @@
             if (error)
                 throw new RuntimeException("Failed. Check output");
 
-        } finally {
-            ((ExecutorService) client.executor()).shutdownNow();
         }
     }
 
     static void checkReturn(HttpRequest[] requests) throws InterruptedException {
         // wait for exceptions and check order
+        boolean ok = true;
         for (int j = 0; j < TIMEOUTS.length; j++) {
-            HttpRequest req = queue.take();
-            out.println("Got request from queue " + req + ", order: " + getRequest(req, requests));
+            HttpResult res = queue.take();
+            HttpRequest req = res.request;
+            out.println("Got request from queue " + req + ", order: " + getRequest(req, requests)
+                         + (res.failed == null ? "" : " failed: " + res.failed));
+            ok = ok && res.failed == null;
         }
-        out.println("Return ok");
+        out.println("Return " + (ok ? "ok" : "nok"));
     }
 
     /** Returns the index of the request in the array. */
--- a/test/jdk/java/net/httpclient/SmokeTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SmokeTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -32,7 +32,7 @@
  * @compile ../../../com/sun/net/httpserver/LogFilter.java
  * @compile ../../../com/sun/net/httpserver/EchoHandler.java
  * @compile ../../../com/sun/net/httpserver/FileServerHandler.java
- * @run main/othervm -Djdk.httpclient.HttpClient.log=errors,trace SmokeTest
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,trace SmokeTest
  */
 
 import com.sun.net.httpserver.Headers;
@@ -47,8 +47,6 @@
 import java.net.InetSocketAddress;
 import java.net.PasswordAuthentication;
 import java.net.ProxySelector;
-import java.net.ServerSocket;
-import java.net.Socket;
 import java.net.URI;
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
@@ -81,9 +79,9 @@
 import java.util.List;
 import java.util.Random;
 import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromInputStream;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromInputStream;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.*;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
@@ -172,7 +170,6 @@
                            .build();
 
         try {
-
             test1(httproot + "files/foo.txt", true);
             test1(httproot + "files/foo.txt", false);
             test1(httpsroot + "files/foo.txt", true);
@@ -255,7 +252,10 @@
 
         String body = response.body();
         if (!body.equals("This is foo.txt\r\n")) {
-            throw new RuntimeException();
+            throw new RuntimeException("Did not get expected body: "
+                + "\n\t expected \"This is foo.txt\\r\\n\""
+                + "\n\t received \""
+                + body.replace("\r", "\\r").replace("\n","\\n") + "\"");
         }
 
         // repeat async
@@ -296,14 +296,13 @@
     static void test2a(String s) throws Exception {
         System.out.print("test2a: " + s);
         URI uri = new URI(s);
-        Path p = Util.getTempFile(128 * 1024);
-        //Path p = Util.getTempFile(1 * 1024);
+        Path p = getTempFile(128 * 1024);
 
         HttpRequest request = HttpRequest.newBuilder(uri)
                                          .POST(fromFile(p))
                                          .build();
 
-        Path resp = Util.getTempFile(1); // will be overwritten
+        Path resp = getTempFile(1); // will be overwritten
 
         HttpResponse<Path> response =
                 client.send(request,
@@ -465,7 +464,7 @@
     @SuppressWarnings("rawtypes")
     static void test7(String target) throws Exception {
         System.out.print("test7: " + target);
-        Path requestBody = Util.getTempFile(128 * 1024);
+        Path requestBody = getTempFile(128 * 1024);
         // First test
         URI uri = new URI(target);
         HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
@@ -644,7 +643,7 @@
         ch.setLevel(Level.SEVERE);
         logger.addHandler(ch);
 
-        String root = System.getProperty ("test.src")+ "/docs";
+        String root = System.getProperty ("test.src", ".")+ "/docs";
         InetSocketAddress addr = new InetSocketAddress (0);
         s1 = HttpServer.create (addr, 0);
         if (s1 instanceof HttpsServer) {
@@ -690,167 +689,106 @@
         proxyPort = proxy.getPort();
         System.out.println("Proxy port = " + proxyPort);
     }
-}
 
-class Configurator extends HttpsConfigurator {
-    public Configurator(SSLContext ctx) {
-        super(ctx);
-    }
+    static class RedirectHandler implements HttpHandler {
+        private final String root;
+        private volatile int count = 0;
 
-    public void configure (HttpsParameters params) {
-        params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
-    }
-}
+        RedirectHandler(String root) {
+            this.root = root;
+        }
 
-class UploadServer extends Thread {
-    int statusCode;
-    ServerSocket ss;
-    int port;
-    int size;
-    Object lock;
-    boolean failed = false;
+        @Override
+        public synchronized void handle(HttpExchange t) throws IOException {
+            byte[] buf = new byte[2048];
+            try (InputStream is = t.getRequestBody()) {
+                while (is.read(buf) != -1) ;
+            }
 
-    UploadServer(int size) throws IOException {
-        this.statusCode = statusCode;
-        this.size = size;
-        ss = new ServerSocket(0);
-        port = ss.getLocalPort();
-        lock = new Object();
-    }
+            Headers responseHeaders = t.getResponseHeaders();
 
-    int port() {
-          return port;
-    }
+            if (count++ < 1) {
+                responseHeaders.add("Location", root + "/foo/" + count);
+            } else {
+                responseHeaders.add("Location", SmokeTest.midSizedFilename);
+            }
+            t.sendResponseHeaders(301, -1);
+            t.close();
+        }
 
-    int size() {
-          return size;
-    }
+        int count() {
+            return count;
+        }
 
-    // wait a sec before calling this
-    boolean failed() {
-        synchronized(lock) {
-            return failed;
+        void reset() {
+            count = 0;
         }
     }
 
-    @Override
-    public void run () {
-        int nbytes = 0;
-        Socket s = null;
-
-        synchronized(lock) {
-            try {
-                s = ss.accept();
+    static class RedirectErrorHandler implements HttpHandler {
+        private final String root;
+        private volatile int count = 1;
 
-                InputStream is = s.getInputStream();
-                OutputStream os = s.getOutputStream();
-                os.write("HTTP/1.1 201 OK\r\nContent-length: 0\r\n\r\n".getBytes());
-                int n;
-                byte[] buf = new byte[8000];
-                while ((n=is.read(buf)) != -1) {
-                    nbytes += n;
-                }
-            } catch (IOException e) {
-                System.out.println ("read " + nbytes);
-                System.out.println ("size " + size);
-                failed = nbytes >= size;
-            } finally {
-                try {
-                    ss.close();
-                    if (s != null)
-                        s.close();
-                } catch (IOException e) {}
-            }
+        RedirectErrorHandler(String root) {
+            this.root = root;
         }
-    }
-}
-
-class RedirectHandler implements HttpHandler {
-    String root;
-    volatile int count = 0;
 
-    RedirectHandler(String root) {
-        this.root = root;
-    }
+        synchronized int count() {
+            return count;
+        }
 
-    @Override
-    public synchronized void handle(HttpExchange t)
-        throws IOException
-    {
-        byte[] buf = new byte[2048];
-        try (InputStream is = t.getRequestBody()) {
-            while (is.read(buf) != -1) ;
+        synchronized void increment() {
+            count++;
         }
 
-        Headers responseHeaders = t.getResponseHeaders();
-
-        if (count++ < 1) {
-            responseHeaders.add("Location", root + "/foo/" + count);
-        } else {
-            responseHeaders.add("Location", SmokeTest.midSizedFilename);
-        }
-        t.sendResponseHeaders(301, -1);
-        t.close();
-    }
-
-    int count() {
-        return count;
-    }
+        @Override
+        public synchronized void handle(HttpExchange t) throws IOException {
+            try (InputStream is = t.getRequestBody()) {
+                is.readAllBytes();
+            }
 
-    void reset() {
-        count = 0;
-    }
-}
-
-class RedirectErrorHandler implements HttpHandler {
-    String root;
-    volatile int count = 1;
-
-    RedirectErrorHandler(String root) {
-        this.root = root;
-    }
-
-    synchronized int count() {
-        return count;
+            Headers map = t.getResponseHeaders();
+            String redirect = root + "/foo/" + Integer.toString(count);
+            increment();
+            map.add("Location", redirect);
+            t.sendResponseHeaders(301, -1);
+            t.close();
+        }
     }
 
-    synchronized void increment() {
-        count++;
-    }
+    static class DelayHandler implements HttpHandler {
+
+        CyclicBarrier bar1 = new CyclicBarrier(2);
+        CyclicBarrier bar2 = new CyclicBarrier(2);
+        CyclicBarrier bar3 = new CyclicBarrier(2);
 
-    @Override
-    public synchronized void handle (HttpExchange t)
-        throws IOException
-    {
-        byte[] buf = new byte[2048];
-        try (InputStream is = t.getRequestBody()) {
-            while (is.read(buf) != -1) ;
+        CyclicBarrier barrier1() {
+            return bar1;
+        }
+
+        CyclicBarrier barrier2() {
+            return bar2;
         }
 
-        Headers map = t.getResponseHeaders();
-        String redirect = root + "/foo/" + Integer.toString(count);
-        increment();
-        map.add("Location", redirect);
-        t.sendResponseHeaders(301, -1);
-        t.close();
+        @Override
+        public synchronized void handle(HttpExchange he) throws IOException {
+            he.getRequestBody().readAllBytes();
+            try {
+                bar1.await();
+                bar2.await();
+            } catch (Exception e) { }
+            he.sendResponseHeaders(200, -1); // will probably fail
+            he.close();
+        }
     }
-}
 
-class Util {
-    static byte[] readAll(InputStream is) throws IOException {
-        byte[] buf = new byte[1024];
-        byte[] result = new byte[0];
+    static class Configurator extends HttpsConfigurator {
+        public Configurator(SSLContext ctx) {
+            super(ctx);
+        }
 
-        while (true) {
-            int n = is.read(buf);
-            if (n > 0) {
-                byte[] b1 = new byte[result.length + n];
-                System.arraycopy(result, 0, b1, 0, result.length);
-                System.arraycopy(buf, 0, b1, result.length, n);
-                result = b1;
-            } else if (n == -1) {
-                return result;
-            }
+        public void configure (HttpsParameters params) {
+            params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
         }
     }
 
@@ -858,8 +796,8 @@
         File f = File.createTempFile("test", "txt");
         f.deleteOnExit();
         byte[] buf = new byte[2048];
-        for (int i=0; i<buf.length; i++)
-            buf[i] = (byte)i;
+        for (int i = 0; i < buf.length; i++)
+            buf[i] = (byte) i;
 
         FileOutputStream fos = new FileOutputStream(f);
         while (size > 0) {
@@ -872,33 +810,6 @@
     }
 }
 
-class DelayHandler implements HttpHandler {
-
-    CyclicBarrier bar1 = new CyclicBarrier(2);
-    CyclicBarrier bar2 = new CyclicBarrier(2);
-    CyclicBarrier bar3 = new CyclicBarrier(2);
-
-    CyclicBarrier barrier1() {
-        return bar1;
-    }
-
-    CyclicBarrier barrier2() {
-        return bar2;
-    }
-
-    @Override
-    public synchronized void handle(HttpExchange he) throws IOException {
-        byte[] buf = Util.readAll(he.getRequestBody());
-        try {
-            bar1.await();
-            bar2.await();
-        } catch (Exception e) {}
-        he.sendResponseHeaders(200, -1); // will probably fail
-        he.close();
-    }
-
-}
-
 // check for simple hardcoded sequence and use remote address
 // to check.
 // First 4 requests executed in sequence (should use same connection/address)
--- a/test/jdk/java/net/httpclient/SplitResponse.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponse.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -21,38 +21,51 @@
  * questions.
  */
 
-
 import java.io.IOException;
-import jdk.incubator.http.HttpClient;
-import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
 import java.net.URI;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
+import javax.net.ssl.SSLContext;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+import static java.lang.System.out;
+import static java.lang.String.format;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
 /**
  * @test
  * @bug 8087112
- * @key intermittent
- * @build Server
- * @run main/othervm -Djava.net.HttpClient.log=all SplitResponse
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all SplitResponse
  */
 
 /**
  * Similar test to QuickResponses except that each byte of the response
  * is sent in a separate packet, which tests the stability of the implementation
- * for receiving unusual packet sizes.
+ * for receiving unusual packet sizes. Additionally, tests scenarios there
+ * connections that are retrieved from the connection pool may reach EOF before
+ * being reused.
  */
 public class SplitResponse {
 
-    static Server server;
+    static String response(String body, boolean serverKeepalive) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("HTTP/1.1 200 OK\r\n");
+        if (!serverKeepalive)
+            sb.append("Connection: Close\r\n");
 
-    static String response(String body) {
-        return "HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-length: "
-                + Integer.toString(body.length())
-                + "\r\n\r\n" + body;
+        sb.append("Content-length: ").append(body.length()).append("\r\n");
+        sb.append("\r\n");
+        sb.append(body);
+        return sb.toString();
     }
 
     static final String responses[] = {
@@ -68,59 +81,123 @@
         "Excepteur sint occaecat cupidatat non proident."
     };
 
+    final ServerSocketFactory factory;
+    final SSLContext context;
+    final boolean useSSL;
+    SplitResponse(boolean useSSL) throws IOException {
+        this.useSSL = useSSL;
+        context = new SimpleSSLContext().get();
+        SSLContext.setDefault(context);
+        factory = useSSL ? SSLServerSocketFactory.getDefault()
+                         : ServerSocketFactory.getDefault();
+    }
+
+    public HttpClient newHttpClient() {
+        HttpClient client;
+        if (useSSL) {
+            client = HttpClient.newBuilder()
+                               .sslContext(context)
+                               .build();
+        } else {
+            client = HttpClient.newHttpClient();
+        }
+        return client;
+    }
+
     public static void main(String[] args) throws Exception {
-        server = new Server(0);
+        boolean useSSL = false;
+        if (args != null && args.length == 1) {
+            useSSL = "SSL".equals(args[0]);
+        }
+        SplitResponse sp = new SplitResponse(useSSL);
+
+        for (Version version : Version.values()) {
+            for (boolean serverKeepalive : new boolean[]{ true, false }) {
+                // Note: the mock server doesn't support Keep-Alive, but
+                // pretending that it might exercises code paths in and out of
+                // the connection pool, and retry logic
+                for (boolean async : new boolean[]{ true, false }) {
+                    sp.test(version, serverKeepalive, async);
+                }
+            }
+        }
+    }
+
+    // @Test
+    void test(Version version, boolean serverKeepalive, boolean async)
+        throws Exception
+    {
+        out.println(format("*** version %s, serverKeepAlive: %s, async: %s ***",
+                           version, serverKeepalive, async));
+        MockServer server = new MockServer(0, factory);
         URI uri = new URI(server.getURL());
+        out.println("server is: " + uri);
         server.start();
 
-        HttpClient client = HttpClient.newHttpClient();
-        HttpRequest request = HttpRequest.newBuilder(uri).build();
+        HttpClient client = newHttpClient();
+        HttpRequest request = HttpRequest.newBuilder(uri).version(version).build();
         HttpResponse<String> r;
         CompletableFuture<HttpResponse<String>> cf1;
 
         try {
             for (int i=0; i<responses.length; i++) {
-                cf1 = client.sendAsync(request, asString());
+                out.println("----- iteration " + i + " -----");
                 String body = responses[i];
+                Thread t = sendSplitResponse(response(body, serverKeepalive), server);
 
-                Server.Connection c = server.activity();
-                sendSplitResponse(response(body), c);
-                r = cf1.get();
-                if (r.statusCode()!= 200)
+                if (async) {
+                    out.println("send async: " + request);
+                    cf1 = client.sendAsync(request, asString());
+                    r = cf1.get();
+                } else { // sync
+                    out.println("send sync: " + request);
+                    r = client.send(request, asString());
+                }
+
+                if (r.statusCode() != 200)
                     throw new RuntimeException("Failed");
 
                 String rxbody = r.body();
-                System.out.println("received " + rxbody);
+                out.println("received " + rxbody);
                 if (!rxbody.equals(body))
-                    throw new RuntimeException("Failed");
-                c.close();
+                    throw new RuntimeException(format("Expected:%s, got:%s", body, rxbody));
+
+                t.join();
+                conn.close();
             }
         } finally {
-            Executor def = client.executor();
-            if (def instanceof ExecutorService) {
-                ((ExecutorService)def).shutdownNow();
-            }
+            server.close();
         }
         System.out.println("OK");
     }
 
-    // send the response one byte at a time with a small delay between bytes
-    // to ensure that each byte is read in a separate read
-    static void sendSplitResponse(String s, Server.Connection conn) {
+    // required for cleanup
+    volatile MockServer.Connection conn;
+
+    // Sends the response, mostly, one byte at a time with a small delay
+    // between bytes, to encourage that each byte is read in a separate read
+    Thread sendSplitResponse(String s, MockServer server) {
         System.out.println("Sending: ");
         Thread t = new Thread(() -> {
+            System.out.println("Waiting for server to receive headers");
+            conn = server.activity();
+            System.out.println("Start sending response");
+
             try {
                 int len = s.length();
+                out.println("sending " + s);
                 for (int i = 0; i < len; i++) {
                     String onechar = s.substring(i, i + 1);
                     conn.send(onechar);
-                    Thread.sleep(30);
+                    Thread.sleep(10);
                 }
-                System.out.println("sent");
+                out.println("sent " + s);
             } catch (IOException | InterruptedException e) {
+                throw new RuntimeException(e);
             }
         });
         t.setDaemon(true);
         t.start();
+        return t;
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseSSL.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2017, 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
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all SplitResponseSSL SSL
+ */
+public class SplitResponseSSL {
+    public static void main(String[] args) throws Exception {
+        SplitResponse.main(args);
+    }
+}
--- a/test/jdk/java/net/httpclient/TimeoutOrdering.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/TimeoutOrdering.java	Sun Nov 05 17:32:13 2017 +0000
@@ -135,8 +135,6 @@
             if (error)
                 throw new RuntimeException("Failed. Check output");
 
-        } finally {
-            ((ExecutorService) client.executor()).shutdownNow();
         }
     }
 
--- a/test/jdk/java/net/httpclient/VersionTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/VersionTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -42,8 +42,7 @@
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
 import jdk.incubator.http.HttpResponse;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
-import static jdk.incubator.http.HttpResponse.*;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
--- a/test/jdk/java/net/httpclient/http2/BasicTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/BasicTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
  * @bug 8087112
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest
@@ -39,8 +40,8 @@
 import java.nio.file.*;
 import java.util.concurrent.*;
 import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
@@ -51,7 +52,8 @@
     static int httpPort, httpsPort;
     static Http2TestServer httpServer, httpsServer;
     static HttpClient client = null;
-    static ExecutorService exec;
+    static ExecutorService clientExec;
+    static ExecutorService serverExec;
     static SSLContext sslContext;
 
     static String httpURIString, httpsURIString;
@@ -61,11 +63,11 @@
             SimpleSSLContext sslct = new SimpleSSLContext();
             sslContext = sslct.get();
             client = getClient();
-            httpServer = new Http2TestServer(false, 0, exec, sslContext);
+            httpServer = new Http2TestServer(false, 0, serverExec, sslContext);
             httpServer.addHandler(new Http2EchoHandler(), "/");
             httpPort = httpServer.getAddress().getPort();
 
-            httpsServer = new Http2TestServer(true, 0, exec, sslContext);
+            httpsServer = new Http2TestServer(true, 0, serverExec, sslContext);
             httpsServer.addHandler(new Http2EchoHandler(), "/");
 
             httpsPort = httpsServer.getAddress().getPort();
@@ -98,15 +100,16 @@
         } finally {
             httpServer.stop();
             httpsServer.stop();
-            exec.shutdownNow();
+            //clientExec.shutdown();
         }
     }
 
     static HttpClient getClient() {
         if (client == null) {
-            exec = Executors.newCachedThreadPool();
+            serverExec = Executors.newCachedThreadPool();
+            clientExec = Executors.newCachedThreadPool();
             client = HttpClient.newBuilder()
-                               .executor(exec)
+                               .executor(clientExec)
                                .sslContext(sslContext)
                                .version(HTTP_2)
                                .build();
@@ -170,11 +173,11 @@
                 });
         response.join();
         compareFiles(src, dest);
-        System.err.println("DONE");
+        System.err.println("streamTest: DONE");
     }
 
     static void paramsTest() throws Exception {
-        Http2TestServer server = new Http2TestServer(true, 0, exec, sslContext);
+        Http2TestServer server = new Http2TestServer(true, 0, serverExec, sslContext);
         server.addHandler((t -> {
             SSLSession s = t.getSSLSession();
             String prot = s.getProtocol();
@@ -196,6 +199,7 @@
             throw new RuntimeException("paramsTest failed "
                 + Integer.toString(stat));
         }
+        System.err.println("paramsTest: DONE");
     }
 
     static void simpleTest(boolean secure) throws Exception {
@@ -237,6 +241,6 @@
             Thread.sleep(100);
         }
         CompletableFuture.allOf(responses).join();
-        System.err.println("DONE");
+        System.err.println("simpleTest: DONE");
     }
 }
--- a/test/jdk/java/net/httpclient/http2/ErrorTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
  * @bug 8157105
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  *          java.security.jgss
@@ -45,7 +46,7 @@
 import java.util.concurrent.ExecutorService;
 import jdk.testlibrary.SimpleSSLContext;
 import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 
 import org.testng.annotations.Test;
--- a/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,7 +26,8 @@
  * @bug 8087112 8177935
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors FixedThreadPoolTest
@@ -39,8 +40,8 @@
 import java.nio.file.*;
 import java.util.concurrent.*;
 import jdk.testlibrary.SimpleSSLContext;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromFile;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromFile;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asFile;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
@@ -104,6 +105,13 @@
     static HttpClient getClient() {
         if (client == null) {
             exec = Executors.newCachedThreadPool();
+            // Executor e1 = Executors.newFixedThreadPool(1);
+            // Executor e = (Runnable r) -> e1.execute(() -> {
+            //    System.out.println("[" + Thread.currentThread().getName()
+            //                       + "] Executing: "
+            //                       + r.getClass().getName());
+            //    r.run();
+            // });
             client = HttpClient.newBuilder()
                                .executor(Executors.newFixedThreadPool(2))
                                .sslContext(sslContext)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, 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
+ * @bug 8153353
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * @key randomness
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/SpecHelper.java
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/TestHelper.java
+ * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/BuffersTestingKit.java
+ * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.BinaryPrimitivesTest
+ */
+public class HpackBinaryTestDriver { }
--- a/test/jdk/java/net/httpclient/http2/HpackDriver.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/HpackDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -29,7 +29,6 @@
  * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/SpecHelper.java
  * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/TestHelper.java
  * @compile/module=jdk.incubator.httpclient jdk/incubator/http/internal/hpack/BuffersTestingKit.java
- * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.BinaryPrimitivesTest
  * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.CircularBufferTest
  * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.DecoderTest
  * @run testng/othervm jdk.incubator.httpclient/jdk.incubator.http.internal.hpack.EncoderTest
--- a/test/jdk/java/net/httpclient/http2/NoBody.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/NoBody.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,24 +26,23 @@
  * @bug 8161157
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,frames,errors NoBody
  */
 
-import java.io.IOException;
 import java.net.URI;
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
 import jdk.incubator.http.HttpResponse;
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ExecutorService;
 import jdk.testlibrary.SimpleSSLContext;
 import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
 import org.testng.annotations.Test;
--- a/test/jdk/java/net/httpclient/http2/ProxyTest2.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ProxyTest2.java	Sun Nov 05 17:32:13 2017 +0000
@@ -62,7 +62,8 @@
  *           tunnelling through an HTTP/1.1 proxy.
  * @modules jdk.incubator.httpclient
  * @library /lib/testlibrary server
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @build jdk.testlibrary.SimpleSSLContext ProxyTest2
--- a/test/jdk/java/net/httpclient/http2/RedirectTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/RedirectTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -24,34 +24,30 @@
 /*
  * @test
  * @bug 8156514
- * @key intermittent
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.frame
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @run testng/othervm -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors RedirectTest
  */
 
 import java.net.*;
 import jdk.incubator.http.*;
-import static jdk.incubator.http.HttpClient.Version.HTTP_2;
-import java.nio.file.*;
 import java.util.concurrent.*;
 import java.util.function.*;
 import java.util.Arrays;
 import java.util.Iterator;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import org.testng.annotations.Test;
+import static jdk.incubator.http.HttpClient.Version.HTTP_2;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
-import org.testng.annotations.Test;
-
-@Test
 public class RedirectTest {
     static int httpPort, altPort;
     static Http2TestServer httpServer, altServer;
     static HttpClient client;
-    static ExecutorService exec;
 
     static String httpURIString, altURIString1, altURIString2;
 
@@ -64,21 +60,21 @@
     static void initialize() throws Exception {
         try {
             client = getClient();
-            httpServer = new Http2TestServer(false, 0, exec, null);
+            httpServer = new Http2TestServer(false, 0, null, null);
 
             httpPort = httpServer.getAddress().getPort();
-            altServer = new Http2TestServer(false, 0, exec, null);
+            altServer = new Http2TestServer(false, 0, null, null);
             altPort = altServer.getAddress().getPort();
 
-            // urls are accessed in sequence below
-            // first two on different servers. Third on same server
-            // as second. So, the client should use the same http connection
+            // urls are accessed in sequence below. The first two are on
+            // different servers. Third on same server as second. So, the
+            // client should use the same http connection.
             httpURIString = "http://127.0.0.1:" + httpPort + "/foo/";
             altURIString1 = "http://127.0.0.1:" + altPort + "/redir";
             altURIString2 = "http://127.0.0.1:" + altPort + "/redir/again";
 
-            httpServer.addHandler(new RedirectHandler(sup(altURIString1)), "/foo");
-            altServer.addHandler(new RedirectHandler(sup(altURIString2)), "/redir");
+            httpServer.addHandler(new Http2RedirectHandler(sup(altURIString1)), "/foo");
+            altServer.addHandler(new Http2RedirectHandler(sup(altURIString2)), "/redir");
             altServer.addHandler(new Http2EchoHandler(), "/redir/again");
 
             httpServer.start();
@@ -90,7 +86,7 @@
         }
     }
 
-    @Test(timeOut=3000000)
+    @Test
     public static void test() throws Exception {
         try {
             initialize();
@@ -101,15 +97,12 @@
         } finally {
             httpServer.stop();
             altServer.stop();
-            exec.shutdownNow();
         }
     }
 
     static HttpClient getClient() {
         if (client == null) {
-            exec = Executors.newCachedThreadPool();
             client = HttpClient.newBuilder()
-                               .executor(exec)
                                .followRedirects(HttpClient.Redirect.ALWAYS)
                                .version(HTTP_2)
                                .build();
@@ -137,18 +130,8 @@
         }
     }
 
-    static Void compareFiles(Path path1, Path path2) {
-        return TestUtil.compareFiles(path1, path2);
-    }
-
-    static Path tempFile() {
-        return TestUtil.tempFile();
-    }
-
     static final String SIMPLE_STRING = "Hello world Goodbye world";
 
-    static final int FILESIZE = 64 * 1024 + 200;
-
     static void simpleTest() throws Exception {
         URI uri = getURI();
         System.err.println("Request to " + uri);
--- a/test/jdk/java/net/httpclient/http2/ServerPush.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/ServerPush.java	Sun Nov 05 17:32:13 2017 +0000
@@ -26,10 +26,11 @@
  * @bug 8087112 8159814
  * @library /lib/testlibrary server
  * @build jdk.testlibrary.SimpleSSLContext
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
- * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPush
+ * @run testng/othervm -Djdk.internal.httpclient.hpack.debug=true -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPush
  */
 
 import java.io.*;
@@ -37,7 +38,7 @@
 import java.nio.file.*;
 import java.nio.file.attribute.*;
 import jdk.incubator.http.*;
-import jdk.incubator.http.HttpResponse.MultiProcessor;
+import jdk.incubator.http.HttpResponse.MultiSubscriber;
 import jdk.incubator.http.HttpResponse.BodyHandler;
 import java.util.*;
 import java.util.concurrent.*;
@@ -72,7 +73,7 @@
             CompletableFuture<MultiMapResult<Path>> cf =
                 HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
                     .executor(e).build().sendAsync(
-                        request, MultiProcessor.asMap((req) -> {
+                        request, MultiSubscriber.asMap((req) -> {
                             URI u = req.uri();
                             Path path = Paths.get(dir.toString(), u.getPath());
                             try {
--- a/test/jdk/java/net/httpclient/http2/TLSConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/TLSConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -29,19 +29,19 @@
 import java.net.URISyntaxException;
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
-import jdk.incubator.http.HttpResponse;
+
 import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLSession;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
 /*
  * @test
  * @bug 8150769 8157107
- * @key intermittent
  * @library server
  * @summary Checks that SSL parameters can be set for HTTP/2 connection
- * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @modules java.base/sun.net.www.http
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.common
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
  *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
  * @run main/othervm TLSConnection
--- a/test/jdk/java/net/httpclient/http2/Timeout.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/Timeout.java	Sun Nov 05 17:32:13 2017 +0000
@@ -29,13 +29,12 @@
 import jdk.incubator.http.HttpResponse;
 import jdk.incubator.http.HttpTimeoutException;
 import java.time.Duration;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.CompletionException;
 import javax.net.ssl.SSLServerSocket;
 import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLServerSocketFactory;
 import javax.net.ssl.SSLSocket;
-import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
 import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
 
 /*
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/BinaryPrimitivesTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/BinaryPrimitivesTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -24,6 +24,8 @@
 
 import org.testng.annotations.Test;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
@@ -80,7 +82,7 @@
     // for all x: readInteger(writeInteger(x)) == x
     //
     @Test
-    public void integerIdentity() {
+    public void integerIdentity() throws IOException {
         final int MAX_VALUE = 1 << 22;
         int totalCases = 0;
         int maxFilling = 0;
@@ -119,7 +121,11 @@
                         Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
                         r.configure(N);
                         for (ByteBuffer b : buf) {
-                            r.read(b);
+                            try {
+                                r.read(b);
+                            } catch (IOException e) {
+                                throw new UncheckedIOException(e);
+                            }
                         }
                         assertEquals(r.get(), expected);
                         r.reset();
@@ -155,7 +161,11 @@
                         if (!written) {
                             fail("please increase bb size");
                         }
-                        r.configure(N).read(concat(buf));
+                        try {
+                            r.configure(N).read(concat(buf));
+                        } catch (IOException e) {
+                            throw new UncheckedIOException(e);
+                        }
                         // TODO: check payload here
                         assertEquals(r.get(), expected);
                         w.reset();
@@ -172,7 +182,7 @@
     // for all x: readString(writeString(x)) == x
     //
     @Test
-    public void stringIdentity() {
+    public void stringIdentity() throws IOException {
         final int MAX_STRING_LENGTH = 4096;
         ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE
         CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH);
@@ -241,7 +251,11 @@
                 if (!written) {
                     fail("please increase 'bytes' size");
                 }
-                reader.read(concat(buffers), chars);
+                try {
+                    reader.read(concat(buffers), chars);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
                 chars.flip();
                 assertEquals(chars.toString(), expected);
                 reader.reset();
@@ -279,7 +293,11 @@
             forEachSplit(bytes, (buffers) -> {
                 for (ByteBuffer buf : buffers) {
                     int p0 = buf.position();
-                    reader.read(buf, chars);
+                    try {
+                        reader.read(buf, chars);
+                    } catch (IOException e) {
+                        throw new UncheckedIOException(e);
+                    }
                     buf.position(p0);
                 }
                 chars.flip();
@@ -333,7 +351,11 @@
     private static void verifyRead(byte[] data, int expected, int N) {
         ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length);
         IntegerReader reader = new IntegerReader();
-        reader.configure(N).read(buf);
+        try {
+            reader.configure(N).read(buf);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
         assertEquals(expected, reader.get());
     }
 
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/DecoderTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/DecoderTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -24,8 +24,8 @@
 
 import org.testng.annotations.Test;
 
+import java.io.IOException;
 import java.io.UncheckedIOException;
-import java.net.ProtocolException;
 import java.nio.ByteBuffer;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -400,7 +400,7 @@
     // This test is missing in the spec
     //
     @Test
-    public void sizeUpdate() {
+    public void sizeUpdate() throws IOException {
         Decoder d = new Decoder(4096);
         assertEquals(d.getTable().maxSize(), 4096);
         d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30
@@ -421,20 +421,14 @@
         b.flip();
         {
             Decoder d = new Decoder(4096);
-            UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class,
+            assertVoidThrows(IOException.class,
                     () -> d.decode(b, true, (name, value) -> { }));
-
-            assertNotNull(ex.getCause());
-            assertEquals(ex.getCause().getClass(), ProtocolException.class);
         }
         b.flip();
         {
             Decoder d = new Decoder(4096);
-            UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class,
+            assertVoidThrows(IOException.class,
                     () -> d.decode(b, false, (name, value) -> { }));
-
-            assertNotNull(ex.getCause());
-            assertEquals(ex.getCause().getClass(), ProtocolException.class);
         }
     }
 
@@ -445,10 +439,8 @@
                 (byte) 0b11111111, // indexed
                 (byte) 0b10011010  // 25 + ...
         });
-        UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
-        assertNotNull(e.getCause());
-        assertEquals(e.getCause().getClass(), ProtocolException.class);
         assertExceptionMessageContains(e, "Unexpected end of header block");
     }
 
@@ -471,10 +463,10 @@
                 (byte) 0b00000111
         });
 
-        IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
 
-        assertExceptionMessageContains(e, "index=2147483647");
+        assertExceptionMessageContains(e.getCause(), "index=2147483647");
     }
 
     @Test
@@ -490,7 +482,7 @@
                 (byte) 0b00000111
         });
 
-        IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
 
         assertExceptionMessageContains(e, "Integer overflow");
@@ -507,10 +499,8 @@
                 0b00000000, //  but only 3 octets available...
                 0b00000000  // /
         });
-        UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
-        assertNotNull(e.getCause());
-        assertEquals(e.getCause().getClass(), ProtocolException.class);
         assertExceptionMessageContains(e, "Unexpected end of header block");
     }
 
@@ -527,10 +517,8 @@
                 0b00000000, //  /
                 0b00000000  // /
         });
-        UncheckedIOException e = assertVoidThrows(UncheckedIOException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
-        assertNotNull(e.getCause());
-        assertEquals(e.getCause().getClass(), ProtocolException.class);
         assertExceptionMessageContains(e, "Unexpected end of header block");
     }
 
@@ -547,7 +535,7 @@
                 0b00011001, 0b01001101, (byte) 0b11111111,
                 (byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100
         });
-        IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
 
         assertExceptionMessageContains(e, "Encountered EOS");
@@ -566,7 +554,7 @@
                 0b00011001, 0b01001101, (byte) 0b11111111
                 // len("aei") + len(padding) = (5 + 5 + 5) + (9)
         });
-        IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
 
         assertExceptionMessageContains(e, "Padding is too long", "len=9");
@@ -597,7 +585,7 @@
                 (byte) 0b10000011, // huffman=true, length=3
                 0b00011001, 0b01111010, (byte) 0b11111110
         });
-        IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class,
+        IOException e = assertVoidThrows(IOException.class,
                 () -> d.decode(data, true, nopCallback()));
 
         assertExceptionMessageContains(e, "Not a EOS prefix");
@@ -648,13 +636,17 @@
             Decoder d = supplier.get();
             do {
                 ByteBuffer n = i.next();
-                d.decode(n, !i.hasNext(), (name, value) -> {
-                    if (value == null) {
-                        actual.add(name.toString());
-                    } else {
-                        actual.add(name + ": " + value);
-                    }
-                });
+                try {
+                    d.decode(n, !i.hasNext(), (name, value) -> {
+                        if (value == null) {
+                            actual.add(name.toString());
+                        } else {
+                            actual.add(name + ": " + value);
+                        }
+                    });
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
             } while (i.hasNext());
             assertEquals(d.getTable().getStateString(), expectedHeaderTable);
             assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList);
@@ -671,13 +663,17 @@
         ByteBuffer source = SpecHelper.toBytes(hexdump);
 
         List<String> actual = new LinkedList<>();
-        d.decode(source, true, (name, value) -> {
-            if (value == null) {
-                actual.add(name.toString());
-            } else {
-                actual.add(name + ": " + value);
-            }
-        });
+        try {
+            d.decode(source, true, (name, value) -> {
+                if (value == null) {
+                    actual.add(name.toString());
+                } else {
+                    actual.add(name + ": " + value);
+                }
+            });
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
 
         assertEquals(d.getTable().getStateString(), expectedHeaderTable);
         assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList);
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/EncoderTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/EncoderTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -24,6 +24,7 @@
 
 import org.testng.annotations.Test;
 
+import java.io.IOException;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -520,7 +521,7 @@
     }
 
     @Test
-    public void initialSizeUpdateDefaultEncoder() {
+    public void initialSizeUpdateDefaultEncoder() throws IOException {
         Function<Integer, Encoder> e = Encoder::new;
         testSizeUpdate(e, 1024, asList(), asList(0));
         testSizeUpdate(e, 1024, asList(1024), asList(0));
@@ -531,7 +532,7 @@
     }
 
     @Test
-    public void initialSizeUpdateCustomEncoder() {
+    public void initialSizeUpdateCustomEncoder() throws IOException {
         Function<Integer, Encoder> e = EncoderTest::newCustomEncoder;
         testSizeUpdate(e, 1024, asList(), asList(1024));
         testSizeUpdate(e, 1024, asList(1024), asList(1024));
@@ -542,7 +543,7 @@
     }
 
     @Test
-    public void seriesOfSizeUpdatesDefaultEncoder() {
+    public void seriesOfSizeUpdatesDefaultEncoder() throws IOException {
         Function<Integer, Encoder> e = c -> {
             Encoder encoder = new Encoder(c);
             drainInitialUpdate(encoder);
@@ -563,7 +564,7 @@
     // https://tools.ietf.org/html/rfc7541#section-4.2
     //
     @Test
-    public void seriesOfSizeUpdatesCustomEncoder() {
+    public void seriesOfSizeUpdatesCustomEncoder() throws IOException {
         Function<Integer, Encoder> e = c -> {
             Encoder encoder = newCustomEncoder(c);
             drainInitialUpdate(encoder);
@@ -638,7 +639,7 @@
     private void testSizeUpdate(Function<Integer, Encoder> encoder,
                                 int initialSize,
                                 List<Integer> updates,
-                                List<Integer> expected) {
+                                List<Integer> expected) throws IOException {
         Encoder e = encoder.apply(initialSize);
         updates.forEach(e::setMaxCapacity);
         ByteBuffer b = ByteBuffer.allocate(64);
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HeaderTableTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HeaderTableTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2017, 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
@@ -116,7 +116,7 @@
 
     @Test
     public void staticData() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
 
         Map<String, Integer> minimalIndexes = new HashMap<>();
@@ -159,7 +159,7 @@
     @Test
     public void constructorSetsMaxSize() {
         int size = rnd.nextInt(64);
-        HeaderTable t = new HeaderTable(size);
+        HeaderTable t = new HeaderTable(size, HPACK.getLogger());
         assertEquals(t.size(), 0);
         assertEquals(t.maxSize(), size);
     }
@@ -169,13 +169,13 @@
         int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1]
         IllegalArgumentException e =
                 assertVoidThrows(IllegalArgumentException.class,
-                        () -> new HeaderTable(0).setMaxSize(maxSize));
+                        () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize));
         assertExceptionMessageContains(e, "maxSize");
     }
 
     @Test
     public void zeroMaximumSize() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         table.setMaxSize(0);
         assertEquals(table.maxSize(), 0);
     }
@@ -183,41 +183,41 @@
     @Test
     public void negativeIndex() {
         int idx = -(rnd.nextInt(256) + 1); // [-256, -1]
-        IllegalArgumentException e =
-                assertVoidThrows(IllegalArgumentException.class,
-                        () -> new HeaderTable(0).get(idx));
+        IndexOutOfBoundsException e =
+                assertVoidThrows(IndexOutOfBoundsException.class,
+                        () -> new HeaderTable(0, HPACK.getLogger()).get(idx));
         assertExceptionMessageContains(e, "index");
     }
 
     @Test
     public void zeroIndex() {
-        IllegalArgumentException e =
-                assertThrows(IllegalArgumentException.class,
-                        () -> new HeaderTable(0).get(0));
+        IndexOutOfBoundsException e =
+                assertThrows(IndexOutOfBoundsException.class,
+                        () -> new HeaderTable(0, HPACK.getLogger()).get(0));
         assertExceptionMessageContains(e, "index");
     }
 
     @Test
     public void length() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         assertEquals(table.length(), STATIC_TABLE_LENGTH);
     }
 
     @Test
     public void indexOutsideStaticRange() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         int idx = table.length() + (rnd.nextInt(256) + 1);
-        IllegalArgumentException e =
-                assertThrows(IllegalArgumentException.class,
+        IndexOutOfBoundsException e =
+                assertThrows(IndexOutOfBoundsException.class,
                         () -> table.get(idx));
         assertExceptionMessageContains(e, "index");
     }
 
     @Test
     public void entryPutAfterStaticArea() {
-        HeaderTable table = new HeaderTable(256);
+        HeaderTable table = new HeaderTable(256, HPACK.getLogger());
         int idx = table.length() + 1;
-        assertThrows(IllegalArgumentException.class, () -> table.get(idx));
+        assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx));
 
         byte[] bytes = new byte[32];
         rnd.nextBytes(bytes);
@@ -232,13 +232,13 @@
 
     @Test
     public void staticTableHasZeroSize() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         assertEquals(0, table.size());
     }
 
     @Test
     public void lowerIndexPriority() {
-        HeaderTable table = new HeaderTable(256);
+        HeaderTable table = new HeaderTable(256, HPACK.getLogger());
         int oldLength = table.length();
         table.put("bender", "rodriguez");
         table.put("bender", "rodriguez");
@@ -251,7 +251,7 @@
 
     @Test
     public void lowerIndexPriority2() {
-        HeaderTable table = new HeaderTable(256);
+        HeaderTable table = new HeaderTable(256, HPACK.getLogger());
         int oldLength = table.length();
         int idx = rnd.nextInt(oldLength) + 1;
         HeaderField f = table.get(idx);
@@ -267,7 +267,7 @@
 
     @Test
     public void fifo() {
-        HeaderTable t = new HeaderTable(Integer.MAX_VALUE);
+        HeaderTable t = new HeaderTable(Integer.MAX_VALUE, HPACK.getLogger());
         // Let's add a series of header fields
         int NUM_HEADERS = 32;
         for (int i = 1; i <= NUM_HEADERS; i++) {
@@ -293,7 +293,7 @@
 
     @Test
     public void indexOf() {
-        HeaderTable t = new HeaderTable(Integer.MAX_VALUE);
+        HeaderTable t = new HeaderTable(Integer.MAX_VALUE, HPACK.getLogger());
         // Let's put a series of header fields
         int NUM_HEADERS = 32;
         for (int i = 1; i <= NUM_HEADERS; i++) {
@@ -333,11 +333,13 @@
     }
 
     private void testToString0() {
-        HeaderTable table = new HeaderTable(0);
+        HeaderTable table = new HeaderTable(0, HPACK.getLogger());
         {
-            table.setMaxSize(2048);
-            String expected =
-                    format("entries: %d; used %s/%s (%.1f%%)", 0, 0, 2048, 0.0);
+            int maxSize = 2048;
+            table.setMaxSize(maxSize);
+            String expected = format(
+                    "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+                    0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0);
             assertEquals(expected, table.toString());
         }
 
@@ -353,8 +355,9 @@
             int used = name.length() + value.length() + 32;
             double ratio = used * 100.0 / size;
 
-            String expected =
-                    format("entries: 1; used %s/%s (%.1f%%)", used, size, ratio);
+            String expected = format(
+                    "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+                    1, STATIC_TABLE_LENGTH + 1, used, size, ratio);
             assertEquals(expected, s);
         }
 
@@ -364,14 +367,15 @@
             table.put(":status", "");
             String s = table.toString();
             String expected =
-                    format("entries: %d; used %s/%s (%.1f%%)", 2, 78, 78, 100.0);
+                    format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
+                           2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0);
             assertEquals(expected, s);
         }
     }
 
     @Test
     public void stateString() {
-        HeaderTable table = new HeaderTable(256);
+        HeaderTable table = new HeaderTable(256, HPACK.getLogger());
         table.put("custom-key", "custom-header");
         // @formatter:off
         assertEquals("[  1] (s =  55) custom-key: custom-header\n" +
--- a/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HuffmanTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/jdk.incubator.httpclient/jdk/incubator/http/internal/hpack/HuffmanTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -24,6 +24,8 @@
 
 import org.testng.annotations.Test;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.Stack;
 import java.util.regex.Matcher;
@@ -302,7 +304,7 @@
     // @formatter:on
 
     @Test
-    public void read_table() {
+    public void read_table() throws IOException {
         Pattern line = Pattern.compile(
                 "\\(\\s*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" +
                         "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
@@ -555,7 +557,11 @@
     private static void read(String hexdump, String decoded) {
         ByteBuffer source = SpecHelper.toBytes(hexdump);
         Appendable actual = new StringBuilder();
-        new Huffman.Reader().read(source, actual, true);
+        try {
+            new Huffman.Reader().read(source, actual, true);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
         assertEquals(actual.toString(), decoded);
     }
 
--- a/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java	Sun Nov 05 17:32:13 2017 +0000
@@ -88,7 +88,7 @@
                     return null;
                 }
                 ByteBufferReference[] data = df.getData();
-                int len = Utils.remaining(data);
+                long len = Utils.remaining(data);
                 if ((len == 0) && eof) {
                     return null;
                 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016, 2017, 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.IOException;
+import java.io.InputStream;
+import java.util.function.Supplier;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+
+public class Http2RedirectHandler implements Http2Handler {
+
+    final Supplier<String> supplier;
+
+    public Http2RedirectHandler(Supplier<String> redirectSupplier) {
+        supplier = redirectSupplier;
+    }
+
+    @Override
+    public void handle(Http2TestExchange t) throws IOException {
+        try (InputStream is = t.getRequestBody()) {
+            is.readAllBytes();
+            String location = supplier.get();
+            System.err.println("RedirectHandler received request to " + t.getRequestURI());
+            System.err.println("Redirecting to: " + location);
+            HttpHeadersImpl map1 = t.getResponseHeaders();
+            map1.addHeader("Location", location);
+            t.sendResponseHeaders(301, 0);
+            t.close();
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Sun Nov 05 17:32:13 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -38,17 +38,26 @@
 import java.util.*;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
-
 import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.frame.FramesDecoder;
-
 import jdk.incubator.http.internal.common.BufferHandler;
 import jdk.incubator.http.internal.common.HttpHeadersImpl;
 import jdk.incubator.http.internal.common.Queue;
-import jdk.incubator.http.internal.frame.*;
+import jdk.incubator.http.internal.frame.DataFrame;
+import jdk.incubator.http.internal.frame.FramesDecoder;
+import jdk.incubator.http.internal.frame.FramesEncoder;
+import jdk.incubator.http.internal.frame.GoAwayFrame;
+import jdk.incubator.http.internal.frame.HeaderFrame;
+import jdk.incubator.http.internal.frame.HeadersFrame;
+import jdk.incubator.http.internal.frame.Http2Frame;
+import jdk.incubator.http.internal.frame.PushPromiseFrame;
+import jdk.incubator.http.internal.frame.ResetFrame;
+import jdk.incubator.http.internal.frame.SettingsFrame;
+import jdk.incubator.http.internal.frame.WindowUpdateFrame;
 import jdk.incubator.http.internal.hpack.Decoder;
 import jdk.incubator.http.internal.hpack.DecodingCallback;
 import jdk.incubator.http.internal.hpack.Encoder;
+import sun.net.www.http.ChunkedInputStream;
+import sun.net.www.http.HttpClient;
 import static jdk.incubator.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE;
 
 /**
@@ -88,6 +97,7 @@
         this.streams = Collections.synchronizedMap(new HashMap<>());
         this.outputQ = new Queue<>();
         this.socket = socket;
+        this.socket.setTcpNoDelay(true);
         this.serverSettings = SettingsFrame.getDefaultSettings();
         this.exec = server.exec;
         this.secure = server.secure;
@@ -252,6 +262,20 @@
             int start = buf.arrayOffset() + buf.position();
             c += buf.remaining();
             os.write(ba, start, buf.remaining());
+
+//            System.out.println("writing byte at a time");
+//            while (buf.hasRemaining()) {
+//                byte b = buf.get();
+//                os.write(b);
+//                os.flush();
+//                try {
+//                    Thread.sleep(1);
+//                } catch(InterruptedException e) {
+//                    UncheckedIOException uie = new UncheckedIOException(new IOException(""));
+//                    uie.addSuppressed(e);
+//                    throw uie;
+//                }
+//            }
         }
         os.flush();
         //System.err.printf("TestServer: wrote %d bytes\n", c);
@@ -276,9 +300,11 @@
             frame.streamid(0);
             outputQ.put(frame);
             return;
+        } else if (f instanceof GoAwayFrame) {
+            System.err.println("Closing: "+ f.toString());
+            close();
         }
-        //System.err.println("TestServer: Received ---> " + f.toString());
-        throw new UnsupportedOperationException("Not supported yet.");
+        throw new UnsupportedOperationException("Not supported yet: " + f.toString());
     }
 
     void sendWindowUpdates(int len, int streamid) throws IOException {
@@ -290,7 +316,7 @@
         outputQ.put(wup);
     }
 
-    HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) {
+    HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) throws IOException {
         HttpHeadersImpl headers = new HttpHeadersImpl();
 
         DecodingCallback cb = (name, value) -> {
@@ -428,7 +454,7 @@
         System.err.printf("TestServer: %s %s\n", method, path);
         HttpHeadersImpl rspheaders = new HttpHeadersImpl();
         int winsize = clientSettings.getParameter(
-                        SettingsFrame.INITIAL_WINDOW_SIZE);
+                SettingsFrame.INITIAL_WINDOW_SIZE);
         //System.err.println ("Stream window size = " + winsize);
 
         final InputStream bis;
@@ -497,7 +523,7 @@
                     } else {
                         if (q == null && !pushStreams.contains(stream)) {
                             System.err.printf("Non Headers frame received with"+
-                                " non existing stream (%d) ", frame.streamid());
+                                    " non existing stream (%d) ", frame.streamid());
                             System.err.println(frame);
                             continue;
                         }
@@ -721,13 +747,25 @@
     String readHttp1Request() throws IOException {
         String headers = readUntil(CRLF + CRLF);
         int clen = getContentLength(headers);
-        // read the content.
-        byte[] buf = new byte[clen];
-        is.readNBytes(buf, 0, clen);
+        byte[] buf;
+        if (clen >= 0) {
+            // HTTP/1.1 fixed length content ( may be 0 ), read it
+            buf = new byte[clen];
+            is.readNBytes(buf, 0, clen);
+        } else {
+            //  HTTP/1.1 chunked data, read it
+            buf = readChunkedInputStream(is);
+        }
         String body = new String(buf, StandardCharsets.US_ASCII);
         return headers + body;
     }
 
+    // This is a quick hack to get a chunked input stream reader.
+    private static byte[] readChunkedInputStream(InputStream is) throws IOException {
+        ChunkedInputStream cis = new ChunkedInputStream(is, new HttpClient() {}, null);
+        return cis.readAllBytes();
+    }
+
     void sendHttp1Response(int code, String msg, String... headers) throws IOException {
         StringBuilder sb = new StringBuilder();
         sb.append("HTTP/1.1 ")
@@ -795,13 +833,13 @@
      * @param amount
      */
     synchronized void obtainConnectionWindow(int amount) throws InterruptedException {
-       while (amount > 0) {
-           int n = Math.min(amount, sendWindow);
-           amount -= n;
-           sendWindow -= n;
-           if (amount > 0)
-               wait();
-       }
+        while (amount > 0) {
+            int n = Math.min(amount, sendWindow);
+            amount -= n;
+            sendWindow -= n;
+            if (amount > 0)
+                wait();
+        }
     }
 
     synchronized void updateConnectionWindow(int amount) {
@@ -823,9 +861,9 @@
     }
 
     static class NullInputStream extends InputStream {
-       static final NullInputStream INSTANCE = new NullInputStream();
-       private NullInputStream() {}
-       public int read()      { return -1; }
-       public int available() { return 0;  }
-   }
+        static final NullInputStream INSTANCE = new NullInputStream();
+        private NullInputStream() {}
+        public int read()      { return -1; }
+        public int available() { return 0;  }
+    }
 }
--- a/test/jdk/java/net/httpclient/http2/server/RedirectHandler.java	Sun Nov 05 17:05:57 2017 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * Copyright (c) 2016, 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.*;
-import java.util.function.Supplier;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
-
-public class RedirectHandler implements Http2Handler {
-
-    final Supplier<String> supplier;
-
-    public RedirectHandler(Supplier<String> redirectSupplier) {
-        supplier = redirectSupplier;
-    }
-
-    static String consume(InputStream is) throws IOException {
-        byte[] b = new byte[1024];
-        int i;
-        StringBuilder sb = new StringBuilder();
-
-        while ((i=is.read(b)) != -1) {
-            sb.append(new String(b, 0, i, ISO_8859_1));
-        }
-        is.close();
-        return sb.toString();
-    }
-
-    @Override
-    public void handle(Http2TestExchange t) throws IOException {
-        try {
-            consume(t.getRequestBody());
-            String location = supplier.get();
-            System.err.println("RedirectHandler received request to " + t.getRequestURI());
-            System.err.println("Redirecting to: " + location);
-            HttpHeadersImpl map1 = t.getResponseHeaders();
-            map1.addHeader("Location", location);
-            t.sendResponseHeaders(301, 0);
-            // return the number of bytes received (no echo)
-            t.close();
-        } catch (Throwable e) {
-            e.printStackTrace();
-            throw new IOException(e);
-        }
-    }
-}
--- a/test/jdk/java/net/httpclient/security/Driver.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Driver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -34,7 +34,7 @@
  * @compile ../ProxyServer.java
  * @build Security
  *
- * @run driver/timeout=90 Driver
+ * @run main/othervm Driver
  */
 
 /**
@@ -142,11 +142,15 @@
                 .redirectErrorStream(true);
 
             String cmdLine = cmd.stream().collect(Collectors.joining(" "));
+            long start = System.currentTimeMillis();
             Process child = processBuilder.start();
             Logger log = new Logger(cmdLine, child, testClasses);
             log.start();
             retval = child.waitFor();
-            System.out.println("retval = " + retval);
+            long elapsed = System.currentTimeMillis() - start;
+            System.out.println("Security " + testnum
+                               + ": retval = " + retval
+                               + ", duration=" + elapsed+" ms");
         }
         if (retval != 0) {
             Thread.sleep(2000);
--- a/test/jdk/java/net/httpclient/security/Security.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Security.java	Sun Nov 05 17:32:13 2017 +0000
@@ -82,7 +82,6 @@
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Flow;
@@ -297,15 +296,15 @@
                 CompletableFuture<HttpResponse<String>> cf =
                     client.sendAsync(request, new HttpResponse.BodyHandler<String>() {
                         @Override
-                        public HttpResponse.BodyProcessor<String> apply(int status, HttpHeaders responseHeaders)  {
-                            final HttpResponse.BodyProcessor<String> stproc = sth.apply(status, responseHeaders);
-                            return new HttpResponse.BodyProcessor<String>() {
+                        public HttpResponse.BodySubscriber<String> apply(int status, HttpHeaders responseHeaders)  {
+                            final HttpResponse.BodySubscriber<String> stproc = sth.apply(status, responseHeaders);
+                            return new HttpResponse.BodySubscriber<String>() {
                                 @Override
                                 public CompletionStage<String> getBody() {
                                     return stproc.getBody();
                                 }
                                 @Override
-                                public void onNext(ByteBuffer item) {
+                                public void onNext(List<ByteBuffer> item) {
                                     SecurityManager sm = System.getSecurityManager();
                                     // should succeed.
                                     sm.checkPermission(new RuntimePermission("foobar"));
@@ -337,6 +336,9 @@
                     Throwable t = e.getCause();
                     if (t instanceof SecurityException)
                         throw (SecurityException)t;
+                    else if ((t instanceof IOException)
+                              && (t.getCause() instanceof SecurityException))
+                        throw ((SecurityException)t.getCause());
                     else
                         throw new RuntimeException(t);
                 }
@@ -419,12 +421,6 @@
         } finally {
             s1.stop(0);
             executor.shutdownNow();
-            for (HttpClient client : clients) {
-                Executor e = client.executor();
-                if (e instanceof ExecutorService) {
-                    ((ExecutorService)e).shutdownNow();
-                }
-            }
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/security/filePerms/FileProcessorPermissionTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2017, 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 Basic checks for SecurityException from body processors APIs
+ * @run testng/othervm/java.security.policy=httpclient.policy FileProcessorPermissionTest
+ */
+
+import java.io.FilePermission;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.Permission;
+import java.security.Permissions;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.ProtectionDomain;
+import java.util.List;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import org.testng.annotations.Test;
+import static java.nio.file.StandardOpenOption.*;
+import static org.testng.Assert.*;
+
+public class FileProcessorPermissionTest {
+
+    static final String testSrc = System.getProperty("test.src", ".");
+    static final Path fromFilePath = Paths.get(testSrc, "FileProcessorPermissionTest.java");
+    static final Path asFilePath = Paths.get(testSrc, "asFile.txt");
+    static final Path CWD = Paths.get(".");
+    static final Class<SecurityException> SE = SecurityException.class;
+
+    static AccessControlContext withPermissions(Permission... perms) {
+        Permissions p = new Permissions();
+        for (Permission perm : perms) {
+            p.add(perm);
+        }
+        ProtectionDomain pd = new ProtectionDomain(null, p);
+        return new AccessControlContext(new ProtectionDomain[]{ pd });
+    }
+
+    static AccessControlContext noPermissions() {
+        return withPermissions(/*empty*/);
+    }
+
+    @Test
+    public void test() throws Exception {
+        List<PrivilegedExceptionAction<?>> list = List.of(
+                () -> HttpRequest.BodyPublisher.fromFile(fromFilePath),
+
+                () -> HttpResponse.BodyHandler.asFile(asFilePath),
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE),
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE),
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE, READ),
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, CREATE, WRITE, READ, DELETE_ON_CLOSE),
+
+                () -> HttpResponse.BodyHandler.asFileDownload(CWD),
+                () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE),
+                () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE),
+                () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE, READ),
+                () -> HttpResponse.BodyHandler.asFileDownload(CWD, CREATE, WRITE, READ, DELETE_ON_CLOSE),
+
+                // TODO: what do these even mean by themselves, maybe ok means nothing?
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, DELETE_ON_CLOSE),
+                () -> HttpResponse.BodyHandler.asFile(asFilePath, READ)
+        );
+
+        // sanity, just run http ( no security manager )
+        System.setSecurityManager(null);
+        try {
+            for (PrivilegedExceptionAction pa : list) {
+                AccessController.doPrivileged(pa);
+            }
+        } finally {
+            System.setSecurityManager(new SecurityManager());
+        }
+
+        // Run with all permissions, i.e. no further restrictions than test's AllPermission
+        for (PrivilegedExceptionAction pa : list) {
+            try {
+                assert System.getSecurityManager() != null;
+                AccessController.doPrivileged(pa, null, new Permission[] { });
+            } catch (PrivilegedActionException pae) {
+                fail("UNEXPECTED Exception:" + pae);
+                pae.printStackTrace();
+            }
+        }
+
+        // Run with limited permissions, i.e. just what is required
+        AccessControlContext minimalACC = withPermissions(
+                new FilePermission(fromFilePath.toString() , "read"),
+                new FilePermission(asFilePath.toString(), "read,write,delete"),
+                new FilePermission(CWD.toString(), "read,write,delete")
+        );
+        for (PrivilegedExceptionAction pa : list) {
+            try {
+                assert System.getSecurityManager() != null;
+                AccessController.doPrivileged(pa, minimalACC);
+            } catch (PrivilegedActionException pae) {
+                fail("UNEXPECTED Exception:" + pae);
+                pae.printStackTrace();
+            }
+        }
+
+        // Run with NO permissions, i.e. expect SecurityException
+        for (PrivilegedExceptionAction pa : list) {
+            try {
+                assert System.getSecurityManager() != null;
+                AccessController.doPrivileged(pa, noPermissions());
+                fail("EXPECTED SecurityException");
+            } catch (SecurityException expected) {
+                System.out.println("Caught expected SE:" + expected);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/security/filePerms/httpclient.policy	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,46 @@
+grant codeBase "jrt:/jdk.incubator.httpclient" {
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net";
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net.util";
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net.www";
+    permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.misc";
+
+    // ## why is SP not good enough. Check API @throws signatures and impl
+    permission java.net.SocketPermission "*","connect,resolve";
+    permission java.net.URLPermission "http:*","*:*";
+    permission java.net.URLPermission "https:*","*:*";
+    permission java.net.URLPermission "ws:*","*:*";
+    permission java.net.URLPermission "wss:*","*:*";
+    permission java.net.URLPermission "socket:*","CONNECT";  // proxy
+
+    // For request/response body processors, fromFile, asFile
+    permission java.io.FilePermission "<<ALL FILES>>","read,write,delete";
+
+    // ## look at the different property names!
+    permission java.util.PropertyPermission "jdk.httpclient.HttpClient.log","read";  // name!
+    permission java.util.PropertyPermission "jdk.httpclient.auth.retrylimit","read";
+    permission java.util.PropertyPermission "jdk.httpclient.connectionWindowSize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.enablepush","read";
+    permission java.util.PropertyPermission "jdk.httpclient.hpack.maxheadertablesize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.keepalive.timeout","read";
+    permission java.util.PropertyPermission "jdk.httpclient.maxframesize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.maxstreams","read";
+    permission java.util.PropertyPermission "jdk.httpclient.redirects.retrylimit","read";
+    permission java.util.PropertyPermission "jdk.httpclient.windowsize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.bufsize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.internal.selector.timeout","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.debug","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.debug","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.log.level","read";
+
+    // ## these permissions do not appear in the NetPermission spec!!! JDK bug?
+    permission java.net.NetPermission "getSSLContext";
+    permission java.net.NetPermission "setSSLContext";
+
+    permission java.security.SecurityPermission "createAccessControlContext";
+};
+
+// bootstrap to get the test going, it will do its own restrictions
+grant codeBase "file:${test.classes}/*" {
+    permission java.security.AllPermission;
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2017, 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
+ * @bug 8187044 8187111
+ * @summary Verifies that the ConnectionPool correctly handle
+ *          connection deadlines and purges the right connections
+ *          from the cache.
+ * @modules jdk.incubator.httpclient java.management
+ * @run main/othervm --add-reads jdk.incubator.httpclient=java.management jdk.incubator.httpclient/jdk.incubator.http.ConnectionPoolTest
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017, 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
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.internal.common.DemandTest
+ */
--- a/test/jdk/java/net/httpclient/whitebox/Driver.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Driver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -23,10 +23,9 @@
 
 /*
  * @test
- * @bug 8151299 8164704 8187044
- * @modules jdk.incubator.httpclient java.management
+ * @bug 8151299 8164704
+ * @modules jdk.incubator.httpclient
  * @run testng jdk.incubator.httpclient/jdk.incubator.http.SelectorTest
  * @run testng jdk.incubator.httpclient/jdk.incubator.http.RawChannelTest
  * @run testng jdk.incubator.httpclient/jdk.incubator.http.ResponseHeadersTest
- * @run main/othervm --add-reads jdk.incubator.httpclient=java.management jdk.incubator.httpclient/jdk.incubator.http.ConnectionPoolTest
  */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, 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
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.FlowTest
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017, 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
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.Http1HeaderParserTest
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2017, 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
+ * @modules jdk.incubator.httpclient
+ * @ignore
+ */
+
+ // FIXME * @run testng jdk.incubator.httpclient/jdk.incubator.http.SSLTubeTest
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, 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
+ * @modules jdk.incubator.httpclient
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.WrapperTest
+ */
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -25,27 +25,31 @@
 
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
 import java.net.Authenticator;
 import java.net.CookieManager;
 import java.net.InetSocketAddress;
 import java.net.ProxySelector;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
+import java.util.List;
 import java.util.Optional;
+import java.util.Random;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.stream.IntStream;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLParameters;
 import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.FlowTube;
 
 /**
- * @summary Verifies that the ConnectionPool won't prevent an HttpClient
- *          from being GC'ed. Verifies that the ConnectionPool has at most
- *          one CacheCleaner thread running.
- * @bug 8187044
+ * @summary Verifies that the ConnectionPool correctly handle
+ *          connection deadlines and purges the right connections
+ *          from the cache.
+ * @bug 8187044 8187111
  * @author danielfuchs
  */
 public class ConnectionPoolTest {
@@ -69,96 +73,95 @@
         HttpClient client = new HttpClientStub(pool);
         InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
         System.out.println("Adding 10 connections to pool");
-        for (int i=0; i<10; i++) {
-            InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
-            HttpConnection c1 = new HttpConnectionStub(client, addr, proxy, true);
-            pool.returnToPool(c1);
+        Random random = new Random();
+
+        final int count = 20;
+        Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        int[] keepAlives = new int[count];
+        HttpConnectionStub[] connections = new HttpConnectionStub[count];
+        long purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+        long expected = 0;
+        if (purge != expected) {
+            throw new RuntimeException("Bad purge delay: " + purge
+                                        + ", expected " + expected);
         }
-        while (getActiveCleaners() == 0) {
-            System.out.println("Waiting for cleaner to start");
-            Thread.sleep(10);
-        }
-        System.out.println("Active CacheCleaners: " + getActiveCleaners());
-        if (getActiveCleaners() > 1) {
-            throw new RuntimeException("Too many CacheCleaner active: "
-                    + getActiveCleaners());
-        }
-        System.out.println("Removing 9 connections from pool");
-        for (int i=0; i<9; i++) {
+        expected = Long.MAX_VALUE;
+        for (int i=0; i<count; i++) {
             InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
-            HttpConnection c2 = pool.getConnection(true, addr, proxy);
-            if (c2 == null) {
-                throw new RuntimeException("connection not found for " + addr);
-            }
-        }
-        System.out.println("Active CacheCleaners: " + getActiveCleaners());
-        if (getActiveCleaners() != 1) {
-            throw new RuntimeException("Wrong number of CacheCleaner active: "
-                    + getActiveCleaners());
-        }
-        System.out.println("Removing last connection from pool");
-        for (int i=9; i<10; i++) {
-            InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
-            HttpConnection c2 = pool.getConnection(true, addr, proxy);
-            if (c2 == null) {
-                throw new RuntimeException("connection not found for " + addr);
+            keepAlives[i] = random.nextInt(10) * 10  + 10;
+            connections[i] = new HttpConnectionStub(client, addr, proxy, true);
+            System.out.println("Adding connection: " + now
+                                + " keepAlive: " + keepAlives[i]
+                                + " /" + connections[i]);
+            pool.returnToPool(connections[i], now, keepAlives[i]);
+            expected = Math.min(expected, keepAlives[i] * 1000);
+            purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+            if (purge != expected) {
+                throw new RuntimeException("Bad purge delay: " + purge
+                                        + ", expected " + expected);
             }
         }
-        System.out.println("Active CacheCleaners: " + getActiveCleaners()
-                + " (may be 0 or may still be 1)");
-        if (getActiveCleaners() > 1) {
-            throw new RuntimeException("Too many CacheCleaner active: "
-                    + getActiveCleaners());
+        int min = IntStream.of(keepAlives).min().getAsInt();
+        int max = IntStream.of(keepAlives).max().getAsInt();
+        int mean = (min + max)/2;
+        System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
+        purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+        System.out.println("first purge would be in " + purge + " ms");
+        if (Math.abs(purge/1000 - min) > 0) {
+            throw new RuntimeException("expected " + min + " got " + purge/1000);
         }
-        InetSocketAddress addr = InetSocketAddress.createUnresolved("foo", 80);
-        HttpConnection c = new HttpConnectionStub(client, addr, proxy, true);
-        System.out.println("Adding/Removing one connection from pool 20 times in a loop");
-        for (int i=0; i<20; i++) {
-            pool.returnToPool(c);
-            HttpConnection c2 = pool.getConnection(true, addr, proxy);
-            if (c2 == null) {
-                throw new RuntimeException("connection not found for " + addr);
-            }
-            if (c2 != c) {
-                throw new RuntimeException("wrong connection found for " + addr);
-            }
-        }
-        if (getActiveCleaners() > 1) {
-            throw new RuntimeException("Too many CacheCleaner active: "
-                    + getActiveCleaners());
+        long opened = java.util.stream.Stream.of(connections)
+                     .filter(HttpConnectionStub::connected).count();
+        if (opened != count) {
+            throw new RuntimeException("Opened: expected "
+                                       + count + " got " + opened);
         }
-        ReferenceQueue<HttpClient> queue = new ReferenceQueue<>();
-        WeakReference<HttpClient> weak = new WeakReference<>(client, queue);
-        System.gc();
-        Reference.reachabilityFence(pool);
-        client = null; pool = null; c = null;
-        while (true) {
-            long cleaners = getActiveCleaners();
-            System.out.println("Waiting for GC to release stub HttpClient;"
-                    + " active cache cleaners: " + cleaners);
-            System.gc();
-            Reference<?> ref = queue.remove(1000);
-            if (ref == weak) {
-                System.out.println("Stub HttpClient GC'ed");
-                break;
-            }
-        }
-        while (getActiveCleaners() > 0) {
-            System.out.println("Waiting for CacheCleaner to stop");
-            Thread.sleep(1000);
-        }
-        System.out.println("Active CacheCleaners: "
-                + getActiveCleaners());
-
-        if (getActiveCleaners() > 0) {
-            throw new RuntimeException("Too many CacheCleaner active: "
-                    + getActiveCleaners());
+        purge = mean * 1000;
+        System.out.println("start purging at " + purge + " ms");
+        Instant next = now;
+        do {
+           System.out.println("next purge is in " + purge + " ms");
+           next = next.plus(purge, ChronoUnit.MILLIS);
+           purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
+           long k = now.until(next, ChronoUnit.SECONDS);
+           System.out.println("now is " + k + "s from start");
+           for (int i=0; i<count; i++) {
+               if (connections[i].connected() != (k < keepAlives[i])) {
+                   throw new RuntimeException("Bad connection state for "
+                             + i
+                             + "\n\t connected=" + connections[i].connected()
+                             + "\n\t keepAlive=" + keepAlives[i]
+                             + "\n\t elapsed=" + k);
+               }
+           }
+        } while (purge > 0);
+        opened = java.util.stream.Stream.of(connections)
+                     .filter(HttpConnectionStub::connected).count();
+        if (opened != 0) {
+           throw new RuntimeException("Closed: expected "
+                                       + count + " got "
+                                       + (count-opened));
         }
     }
+
     static <T> T error() {
         throw new InternalError("Should not reach here: wrong test assumptions!");
     }
 
+    static class FlowTubeStub implements FlowTube {
+        final HttpConnectionStub conn;
+        FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) { }
+        @Override public void onError(Throwable error) { error(); }
+        @Override public void onComplete() { error(); }
+        @Override public void onNext(List<ByteBuffer> item) { error();}
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+        }
+        @Override public boolean isFinished() { return conn.closed; }
+    }
+
     // Emulates an HttpConnection that has a strong reference to its HttpClient.
     static class HttpConnectionStub extends HttpConnection {
 
@@ -172,50 +175,54 @@
             this.proxy = proxy;
             this.secured = secured;
             this.client = client;
+            this.flow = new FlowTubeStub(this);
         }
 
-        InetSocketAddress proxy;
-        InetSocketAddress address;
-        boolean secured;
-        ConnectionPool.CacheKey key;
-        HttpClient client;
+        final InetSocketAddress proxy;
+        final InetSocketAddress address;
+        final boolean secured;
+        final ConnectionPool.CacheKey key;
+        final HttpClient client;
+        final FlowTubeStub flow;
+        volatile boolean closed;
 
         // All these return something
-        @Override boolean connected() {return true;}
+        @Override boolean connected() {return !closed;}
         @Override boolean isSecure() {return secured;}
         @Override boolean isProxied() {return proxy!=null;}
         @Override ConnectionPool.CacheKey cacheKey() {return key;}
-        @Override public void close() {}
         @Override void shutdownInput() throws IOException {}
         @Override void shutdownOutput() throws IOException {}
+        @Override
+        public void close() {
+            closed=true;
+            System.out.println("closed: " + this);
+        }
+        @Override
         public String toString() {
             return "HttpConnectionStub: " + address + " proxy: " + proxy;
         }
 
         // All these throw errors
-        @Override
-        public void connect() throws IOException, InterruptedException {error();}
+        @Override public HttpPublisher publisher() {return error();}
         @Override public CompletableFuture<Void> connectAsync() {return error();}
         @Override SocketChannel channel() {return error();}
-        @Override void flushAsync() throws IOException {error();}
-        @Override
-        protected ByteBuffer readImpl() throws IOException {return error();}
-        @Override CompletableFuture<Void> whenReceivingResponse() {return error();}
+        @Override public void flushAsync() throws IOException {error();}
         @Override
-        long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-            throw (Error)error();
-        }
-        @Override
-        long write(ByteBuffer buffer) throws IOException {throw (Error)error();}
-        @Override
-        void writeAsync(ByteBufferReference[] buffers) throws IOException {
+        public void writeAsync(ByteBufferReference[] buffers) throws IOException {
             error();
         }
         @Override
-        void writeAsyncUnordered(ByteBufferReference[] buffers)
+        public void writeAsyncUnordered(ByteBufferReference[] buffers)
                 throws IOException {
             error();
         }
+        @Override
+        HttpConnection.DetachedConnectionChannel detachChannel() {
+            return error();
+        }
+        @Override
+        FlowTube getConnectionFlow() {return flow;}
     }
     // Emulates an HttpClient that has a strong reference to its connection pool.
     static class HttpClientStub extends HttpClient {
@@ -227,10 +234,10 @@
         @Override public HttpClient.Redirect followRedirects() {return error();}
         @Override public Optional<ProxySelector> proxy() {return error();}
         @Override public SSLContext sslContext() {return error();}
-        @Override public Optional<SSLParameters> sslParameters() {return error();}
+        @Override public SSLParameters sslParameters() {return error();}
         @Override public Optional<Authenticator> authenticator() {return error();}
         @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
-        @Override public Executor executor() {return error();}
+        @Override public Optional<Executor> executor() {return error();}
         @Override
         public <T> HttpResponse<T> send(HttpRequest req,
                 HttpResponse.BodyHandler<T> responseBodyHandler)
@@ -244,7 +251,7 @@
         }
         @Override
         public <U, T> CompletableFuture<U> sendAsync(HttpRequest req,
-                HttpResponse.MultiProcessor<U, T> multiProcessor) {
+                HttpResponse.MultiSubscriber<U, T> multiSubscriber) {
             return error();
         }
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/FlowTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2017, 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.
+ */
+
+package jdk.incubator.http;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Random;
+import java.util.StringTokenizer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.SubmissionPublisher;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.*;
+import javax.net.ssl.TrustManagerFactory;
+import jdk.incubator.http.internal.common.Utils;
+import org.testng.annotations.Test;
+import jdk.incubator.http.internal.common.SSLFlowDelegate;
+
+@Test
+public class FlowTest {
+
+    private final SubmissionPublisher<List<ByteBuffer>> srcPublisher;
+    private final ExecutorService executor;
+    private static final long COUNTER = 3000;
+    private static final int LONGS_PER_BUF = 800;
+    static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF;
+    public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0);
+    static volatile String alpn;
+
+    private final CompletableFuture<Void> completion;
+
+    public FlowTest() throws IOException {
+        executor = Executors.newCachedThreadPool();
+        srcPublisher = new SubmissionPublisher<>(executor, 20,
+                                                 this::handlePublisherException);
+        SSLContext ctx = (new SimpleSSLContext()).get();
+        SSLEngine engineClient = ctx.createSSLEngine();
+        SSLParameters params = ctx.getSupportedSSLParameters();
+        params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2
+        params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl
+        engineClient.setSSLParameters(params);
+        engineClient.setUseClientMode(true);
+        completion = new CompletableFuture<>();
+        SSLLoopbackSubscriber looper = new SSLLoopbackSubscriber(ctx, executor);
+        looper.start();
+        EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion);
+        SSLFlowDelegate sslClient = new SSLFlowDelegate(engineClient, executor, end, looper);
+        // going to measure how long handshake takes
+        final long start = System.currentTimeMillis();
+        sslClient.alpn().whenComplete((String s, Throwable t) -> {
+            if (t != null)
+                t.printStackTrace();
+            long endTime = System.currentTimeMillis();
+            alpn = s;
+            System.out.println("ALPN: " + alpn);
+            long period = (endTime - start);
+            System.out.printf("Handshake took %d ms\n", period);
+        });
+        Subscriber<List<ByteBuffer>> reader = sslClient.upstreamReader();
+        Subscriber<List<ByteBuffer>> writer = sslClient.upstreamWriter();
+        looper.setReturnSubscriber(reader);
+        // now connect all the pieces
+        srcPublisher.subscribe(writer);
+        String aa = sslClient.alpn().join();
+        System.out.println("AAALPN = " + aa);
+    }
+
+    static Random rand = new Random();
+
+    static int randomRange(int lower, int upper) {
+        if (lower > upper)
+            throw new IllegalArgumentException("lower > upper");
+        int diff = upper - lower;
+        int r = lower + rand.nextInt(diff);
+        return r - (r % 8); // round down to multiple of 8 (align for longs)
+    }
+
+    private void handlePublisherException(Object o, Throwable t) {
+        System.out.println("Src Publisher exception");
+        t.printStackTrace(System.out);
+    }
+
+    private static ByteBuffer getBuffer(long startingAt) {
+        ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8);
+        for (int j = 0; j < LONGS_PER_BUF; j++) {
+            buf.putLong(startingAt++);
+        }
+        buf.flip();
+        return buf;
+    }
+
+    @Test
+    public void run() {
+        long count = 0;
+        System.out.printf("Submitting %d buffer arrays\n", COUNTER);
+        System.out.printf("LoopCount should be %d\n", TOTAL_LONGS);
+        for (long i = 0; i < COUNTER; i++) {
+            ByteBuffer b = getBuffer(count);
+            count += LONGS_PER_BUF;
+            srcPublisher.submit(List.of(b));
+        }
+        System.out.println("Finished submission. Waiting for loopback");
+        srcPublisher.close();
+        try {
+            completion.join();
+            if (!alpn.equals("proto2")) {
+                throw new RuntimeException("wrong alpn received");
+            }
+            System.out.println("OK");
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+/*
+    public static void main(String[]args) throws Exception {
+        FlowTest test = new FlowTest();
+        test.run();
+    }
+*/
+
+    /**
+     * This Subscriber simulates an SSL loopback network. The object itself
+     * accepts outgoing SSL encrypted data which is looped back via two sockets
+     * (one of which is an SSLSocket emulating a server). The method
+     * {@link #setReturnSubscriber(java.util.concurrent.Flow.Subscriber) }
+     * is used to provide the Subscriber which feeds the incoming side
+     * of SSLFlowDelegate. Three threads are used to implement this behavior
+     * and a SubmissionPublisher drives the incoming read side.
+     * <p>
+     * A thread reads from the buffer, writes
+     * to the client j.n.Socket which is connected to a SSLSocket operating
+     * in server mode. A second thread loops back data read from the SSLSocket back to the
+     * client again. A third thread reads the client socket and pushes the data to
+     * a SubmissionPublisher that drives the reader side of the SSLFlowDelegate
+     */
+    static class SSLLoopbackSubscriber implements Subscriber<List<ByteBuffer>> {
+        private final BlockingQueue<ByteBuffer> buffer;
+        private final Socket clientSock;
+        private final SSLSocket serverSock;
+        private final Thread thread1, thread2, thread3;
+        private volatile Flow.Subscription clientSubscription;
+        private final SubmissionPublisher<List<ByteBuffer>> publisher;
+
+        SSLLoopbackSubscriber(SSLContext ctx, ExecutorService exec) throws IOException {
+            SSLServerSocketFactory fac = ctx.getServerSocketFactory();
+            SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0);
+            SSLParameters params = serv.getSSLParameters();
+            params.setApplicationProtocols(new String[]{"proto2"});
+            serv.setSSLParameters(params);
+
+
+            int serverPort = serv.getLocalPort();
+            clientSock = new Socket("127.0.0.1", serverPort);
+            serverSock = (SSLSocket) serv.accept();
+            this.buffer = new LinkedBlockingQueue<>();
+            thread1 = new Thread(this::clientWriter, "clientWriter");
+            thread2 = new Thread(this::serverLoopback, "serverLoopback");
+            thread3 = new Thread(this::clientReader, "clientReader");
+            publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(),
+                    this::handlePublisherException);
+            SSLFlowDelegate.Monitor.add(this::monitor);
+        }
+
+        public void start() {
+            thread1.start();
+            thread2.start();
+            thread3.start();
+        }
+
+        private void handlePublisherException(Object o, Throwable t) {
+            System.out.println("Loopback Publisher exception");
+            t.printStackTrace(System.out);
+        }
+
+        private final AtomicInteger readCount = new AtomicInteger();
+
+        // reads off the SSLSocket the data from the "server"
+        private void clientReader() {
+            try {
+                InputStream is = clientSock.getInputStream();
+                final int bufsize = FlowTest.randomRange(512, 16 * 1024);
+                System.out.println("clientReader: bufsize = " + bufsize);
+                while (true) {
+                    byte[] buf = new byte[bufsize];
+                    int n = is.read(buf);
+                    if (n == -1) {
+                        System.out.println("clientReader close");
+                        publisher.close();
+                        Utils.sleep(2000);
+                        Utils.close(is, clientSock);
+                        return;
+                    }
+                    ByteBuffer bb = ByteBuffer.wrap(buf, 0, n);
+                    readCount.addAndGet(n);
+                    publisher.submit(List.of(bb));
+                }
+            } catch (Throwable e) {
+                e.printStackTrace();
+                Utils.close(clientSock);
+            }
+        }
+
+        // writes the encrypted data from SSLFLowDelegate to the j.n.Socket
+        // which is connected to the SSLSocket emulating a server.
+        private void clientWriter() {
+            try {
+                OutputStream os =
+                        new BufferedOutputStream(clientSock.getOutputStream());
+
+                while (true) {
+                    ByteBuffer buf = buffer.take();
+                    if (buf == FlowTest.SENTINEL) {
+                        // finished
+                        //Utils.sleep(2000);
+                        System.out.println("clientWriter close");
+                        clientSock.shutdownOutput();
+                        System.out.println("clientWriter close return");
+                        return;
+                    }
+                    writeToStream(os, buf);
+                    clientSubscription.request(1);
+                }
+            } catch (Throwable e) {
+                e.printStackTrace();
+            }
+        }
+
+        private void writeToStream(OutputStream os, ByteBuffer buf) throws IOException {
+            byte[] b = buf.array();
+            int offset = buf.arrayOffset() + buf.position();
+            int n = buf.limit() - buf.position();
+            os.write(b, offset, n);
+            buf.position(buf.limit());
+            os.flush();
+        }
+
+        private final AtomicInteger loopCount = new AtomicInteger();
+
+        public String monitor() {
+            return "serverLoopback: loopcount = " + loopCount.toString()
+                    + " clientRead: count = " + readCount.toString();
+        }
+
+        // thread2
+        private void serverLoopback() {
+            try {
+                InputStream is = serverSock.getInputStream();
+                OutputStream os = serverSock.getOutputStream();
+                final int bufsize = FlowTest.randomRange(512, 16 * 1024);
+                System.out.println("serverLoopback: bufsize = " + bufsize);
+                byte[] bb = new byte[bufsize];
+                while (true) {
+                    int n = is.read(bb);
+                    if (n == -1) {
+                        Utils.sleep(2000);
+                        is.close();
+                        serverSock.close();
+                        return;
+                    }
+                    os.write(bb, 0, n);
+                    os.flush();
+                    loopCount.addAndGet(n);
+                }
+            } catch (Throwable e) {
+                e.printStackTrace();
+            }
+        }
+
+
+        /**
+         * This needs to be called before the chain is subscribed. It can't be
+         * supplied in the constructor.
+         */
+        public void setReturnSubscriber(Subscriber<List<ByteBuffer>> returnSubscriber) {
+            publisher.subscribe(returnSubscriber);
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            clientSubscription = subscription;
+            clientSubscription.request(5);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            try {
+                for (ByteBuffer b : item)
+                    buffer.put(b);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+                Utils.close(clientSock);
+            }
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            throwable.printStackTrace();
+            Utils.close(clientSock);
+        }
+
+        @Override
+        public void onComplete() {
+            try {
+                buffer.put(FlowTest.SENTINEL);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+                Utils.close(clientSock);
+            }
+        }
+    }
+
+    /**
+     * The final subscriber which receives the decrypted looped-back data.
+     * Just needs to compare the data with what was sent. The given CF is
+     * either completed exceptionally with an error or normally on success.
+     */
+    static class EndSubscriber implements Subscriber<List<ByteBuffer>> {
+
+        private final long nbytes;
+
+        private final AtomicLong counter;
+        private volatile Flow.Subscription subscription;
+        private final CompletableFuture<Void> completion;
+
+        EndSubscriber(long nbytes, CompletableFuture<Void> completion) {
+            counter = new AtomicLong(0);
+            this.nbytes = nbytes;
+            this.completion = completion;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            this.subscription = subscription;
+            subscription.request(5);
+        }
+
+        public static String info(List<ByteBuffer> i) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("size: ").append(Integer.toString(i.size()));
+            int x = 0;
+            for (ByteBuffer b : i)
+                x += b.remaining();
+            sb.append(" bytes: " + Integer.toString(x));
+            return sb.toString();
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> buffers) {
+            long currval = counter.get();
+            //if (currval % 500 == 0) {
+            //System.out.println("End: " + currval);
+            //}
+
+            for (ByteBuffer buf : buffers) {
+                while (buf.hasRemaining()) {
+                    long n = buf.getLong();
+                    //if (currval > (FlowTest.TOTAL_LONGS - 50)) {
+                    //System.out.println("End: " + currval);
+                    //}
+                    if (n != currval++) {
+                        System.out.println("ERROR at " + n + " != " + (currval - 1));
+                        completion.completeExceptionally(new RuntimeException("ERROR"));
+                        subscription.cancel();
+                        return;
+                    }
+                }
+            }
+
+            counter.set(currval);
+            subscription.request(1);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            completion.completeExceptionally(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            long n = counter.get();
+            if (n != nbytes) {
+                System.out.printf("nbytes=%d n=%d\n", nbytes, n);
+                completion.completeExceptionally(new RuntimeException("ERROR AT END"));
+            } else {
+                System.out.println("DONE OK: counter = " + n);
+                completion.complete(null);
+            }
+        }
+    }
+
+    /**
+     * Creates a simple usable SSLContext for SSLSocketFactory
+     * or a HttpsServer using either a given keystore or a default
+     * one in the test tree.
+     * <p>
+     * Using this class with a security manager requires the following
+     * permissions to be granted:
+     * <p>
+     * permission "java.util.PropertyPermission" "test.src.path", "read";
+     * permission java.io.FilePermission
+     * "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", "read";
+     * The exact path above depends on the location of the test.
+     */
+    static class SimpleSSLContext {
+
+        private final SSLContext ssl;
+
+        /**
+         * Loads default keystore from SimpleSSLContext source directory
+         */
+        public SimpleSSLContext() throws IOException {
+            String paths = System.getProperty("test.src.path");
+            StringTokenizer st = new StringTokenizer(paths, File.pathSeparator);
+            boolean securityExceptions = false;
+            SSLContext sslContext = null;
+            while (st.hasMoreTokens()) {
+                String path = st.nextToken();
+                try {
+                    File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys");
+                    if (f.exists()) {
+                        try (FileInputStream fis = new FileInputStream(f)) {
+                            sslContext = init(fis);
+                            break;
+                        }
+                    }
+                } catch (SecurityException e) {
+                    // catch and ignore because permission only required
+                    // for one entry on path (at most)
+                    securityExceptions = true;
+                }
+            }
+            if (securityExceptions) {
+                System.out.println("SecurityExceptions thrown on loading testkeys");
+            }
+            ssl = sslContext;
+        }
+
+        private SSLContext init(InputStream i) throws IOException {
+            try {
+                char[] passphrase = "passphrase".toCharArray();
+                KeyStore ks = KeyStore.getInstance("JKS");
+                ks.load(i, passphrase);
+
+                KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+                kmf.init(ks, passphrase);
+
+                TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+                tmf.init(ks);
+
+                SSLContext ssl = SSLContext.getInstance("TLS");
+                ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+                return ssl;
+            } catch (KeyManagementException | KeyStoreException |
+                    UnrecoverableKeyException | CertificateException |
+                    NoSuchAlgorithmException e) {
+                throw new RuntimeException(e.getMessage());
+            }
+        }
+
+        public SSLContext get() {
+            return ssl;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/Http1HeaderParserTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,348 @@
+/*
+ * Copyright (c) 2017, 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.
+ */
+
+package jdk.incubator.http;
+
+import java.io.ByteArrayInputStream;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import sun.net.www.MessageHeader;
+import org.testng.annotations.Test;
+import org.testng.annotations.DataProvider;
+import static java.lang.System.out;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.stream.Collectors.toList;
+import static org.testng.Assert.*;
+
+// Mostly verifies the "new" Http1HeaderParser returns the same results as the
+// tried and tested sun.net.www.MessageHeader.
+
+public class Http1HeaderParserTest {
+
+    @DataProvider(name = "responses")
+    public Object[][] responses() {
+        List<String> responses = new ArrayList<>();
+
+        String[] basic =
+            { "HTTP/1.1 200 OK\r\n\r\n",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
+              "Server: Apache/1.3.14 (Unix)\r\n" +
+              "Connection: close\r\n" +
+              "Content-Type: text/html; charset=iso-8859-1\r\n" +
+              "Content-Length: 10\r\n\r\n" +
+              "123456789",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 9\r\n" +
+              "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+              "XXXXX",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length:   9\r\n" +
+              "Content-Type:   text/html; charset=UTF-8\r\n\r\n" +   // more than one SP after ':'
+              "XXXXX",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length:\t10\r\n" +
+              "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" +   // HT separator
+              "XXXXX",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length:\t\t10\r\n" +
+              "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" +   // more than one HT after ':'
+              "XXXXX",
+
+              "HTTP/1.1 407 Proxy Authorization Required\r\n" +
+              "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
+
+              "HTTP/1.1 401 Unauthorized\r\n" +
+              "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
+              "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo:\r\n\r\n",      // no value
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo:\r\n\r\n" +     // no value, with response body
+              "Some Response Body",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo:\r\n" +    // no value, followed by another header
+              "Content-Length: 10\r\n\r\n" +
+              "Some Response Body",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo:\r\n" +    // no value, followed by another header, with response body
+              "Content-Length: 10\r\n\r\n",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo: chegar\r\n" +
+              "X-Foo: dfuchs\r\n" +  // same header appears multiple times
+              "Content-Length: 0\r\n" +
+              "X-Foo: michaelm\r\n" +
+              "X-Foo: prappo\r\n\r\n",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "X-Foo:\r\n" +    // no value, same header appears multiple times
+              "X-Foo: dfuchs\r\n" +
+              "Content-Length: 0\r\n" +
+              "X-Foo: michaelm\r\n" +
+              "X-Foo: prappo\r\n\r\n",
+            };
+        Arrays.stream(basic).forEach(responses::add);
+
+        String[] foldingTemplate =
+           {  "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 9\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r'
+              " charset=UTF-8\r\n" +                // one preceding SP
+              "Connection: close\r\n\r\n" +
+              "XXYYZZAABBCCDDEE",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 19\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r
+              "   charset=UTF-8\r\n" +              // more than one preceding SP
+              "Connection: keep-alive\r\n\r\n" +
+              "XXYYZZAABBCCDDEEFFGG",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 999\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r
+              "\tcharset=UTF-8\r\n" +               // one preceding HT
+              "Connection: close\r\n\r\n" +
+              "XXYYZZAABBCCDDEE",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 54\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r
+              "\t\t\tcharset=UTF-8\r\n" +           // more than one preceding HT
+              "Connection: keep-alive\r\n\r\n" +
+              "XXYYZZAABBCCDDEEFFGG",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: -1\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r
+              "\t \t \tcharset=UTF-8\r\n" +         // mix of preceding HT and SP
+              "Connection: keep-alive\r\n\r\n" +
+              "XXYYZZAABBCCDDEEFFGGHH",
+
+              "HTTP/1.1 200 OK\r\n" +
+              "Content-Length: 65\r\n" +
+              "Content-Type: text/html;$NEWLINE" +  // folding field-value with '\n'|'\r
+              " \t \t charset=UTF-8\r\n" +          // mix of preceding SP and HT
+              "Connection: keep-alive\r\n\r\n" +
+              "XXYYZZAABBCCDDEEFFGGHHII",
+           };
+        for (String newLineChar : new String[] { "\n", "\r" }) {
+            for (String template : foldingTemplate)
+                responses.add(template.replace("$NEWLINE", newLineChar));
+        }
+
+        String[] bad = // much of this is to retain parity with legacy MessageHeaders
+           { "HTTP/1.1 200 OK\r\n" +
+             "Connection:\r\n\r\n",   // empty value, no body
+
+             "HTTP/1.1 200 OK\r\n" +
+             "Connection:\r\n\r\n" +  // empty value, with body
+             "XXXXX",
+
+             "HTTP/1.1 200 OK\r\n" +
+             ": no header\r\n\r\n",  // no/empty header-name, no body, no following header
+
+             "HTTP/1.1 200 OK\r\n" +
+             ": no; header\r\n" +  // no/empty header-name, no body, following header
+             "Content-Length: 65\r\n\r\n",
+
+             "HTTP/1.1 200 OK\r\n" +
+             ": no header\r\n" +  // no/empty header-name
+             "Content-Length: 65\r\n\r\n" +
+             "XXXXX",
+
+             "HTTP/1.1 200 OK\r\n" +
+             ": no header\r\n\r\n" +  // no/empty header-name, followed by header
+             "XXXXX",
+
+             "HTTP/1.1 200 OK\r\n" +
+             "Conte\r" +
+             " nt-Length: 9\r\n" +    // fold/bad header name ???
+             "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+             "XXXXX",
+
+             "HTTP/1.1 200 OK\r\n" +
+             "Conte\r" +
+             "nt-Length: 9\r\n" +    // fold/bad header name ??? without preceding space
+             "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+             "XXXXXYYZZ",
+
+             "HTTP/1.0 404 Not Found\r\n" +
+             "header-without-colon\r\n\r\n",
+
+             "HTTP/1.0 404 Not Found\r\n" +
+             "header-without-colon\r\n\r\n" +
+             "SOMEBODY",
+
+           };
+        Arrays.stream(bad).forEach(responses::add);
+
+        return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "responses")
+    public void verifyHeaders(String respString) throws Exception {
+        byte[] bytes = respString.getBytes(US_ASCII);
+        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+        MessageHeader m = new MessageHeader(bais);
+        Map<String,List<String>> messageHeaderMap = m.getHeaders();
+        int available = bais.available();
+
+        Http1HeaderParser decoder = new Http1HeaderParser();
+        ByteBuffer b = ByteBuffer.wrap(bytes);
+        decoder.parse(b);
+        Map<String,List<String>> decoderMap1 = decoder.headers().map();
+        assertEquals(available, b.remaining(),
+                     "stream available not equal to remaining");
+
+        // assert status-line
+        String statusLine1 = messageHeaderMap.get(null).get(0);
+        String statusLine2 = decoder.statusLine();
+        if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
+            assertEquals(statusLine1, statusLine2, "Status-line not equal");
+        } else {
+            assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
+        }
+
+        // remove the null'th entry with is the status-line
+        Map<String,List<String>> map = new HashMap<>();
+        for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
+            if (e.getKey() != null) {
+                map.put(e.getKey(), e.getValue());
+            }
+        }
+        messageHeaderMap = map;
+
+        assertHeadersEqual(messageHeaderMap, decoderMap1,
+                          "messageHeaderMap not equal to decoderMap1");
+
+        // byte at a time
+        decoder = new Http1HeaderParser();
+        List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
+                .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
+                .collect(toList());
+        while (decoder.parse(buffers.remove(0)) != true);
+        Map<String,List<String>> decoderMap2 = decoder.headers().map();
+        assertEquals(available, buffers.size(),
+                     "stream available not equals to remaining buffers");
+        assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
+    }
+
+    @DataProvider(name = "errors")
+    public Object[][] errors() {
+        List<String> responses = new ArrayList<>();
+
+        // These responses are parsed, somewhat, by MessageHeaders but give
+        // nonsensible results. They, correctly, fail with the Http1HeaderParser.
+        String[] bad =
+           {// "HTTP/1.1 402 Payment Required\r\n" +
+            // "Content-Length: 65\r\n\r",   // missing trailing LF   //TODO: incomplete
+
+             "HTTP/1.1 402 Payment Required\r\n" +
+             "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
+
+             "HTTP/1.1 200OK\r\n\rT",
+
+             "HTTP/1.1 200OK\rT",
+           };
+        Arrays.stream(bad).forEach(responses::add);
+
+        return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
+    public void errors(String respString) throws ProtocolException {
+        byte[] bytes = respString.getBytes(US_ASCII);
+        Http1HeaderParser decoder = new Http1HeaderParser();
+        ByteBuffer b = ByteBuffer.wrap(bytes);
+        decoder.parse(b);
+    }
+
+    void assertHeadersEqual(Map<String,List<String>> expected,
+                            Map<String,List<String>> actual,
+                            String msg) {
+
+        if (expected.equals(actual))
+            return;
+
+        assertEquals(expected.size(), actual.size(),
+                     format("%s. Expected size %d, actual size %s. expected= %s, actual=%s.",
+                            msg, expected.size(), actual.size(), expected, actual));
+
+        for (Map.Entry<String,List<String>> e : expected.entrySet()) {
+            String key = e.getKey();
+            List<String> values = e.getValue();
+
+            boolean found = false;
+            for (Map.Entry<String,List<String>> other: actual.entrySet()) {
+                if (key.equalsIgnoreCase(other.getKey())) {
+                    found = true;
+                    List<String> otherValues = other.getValue();
+                    assertEquals(values.size(), otherValues.size(),
+                                 format("%s. Expected list size %d, actual size %s",
+                                        msg, values.size(), otherValues.size()));
+                    if (!values.containsAll(otherValues) && otherValues.containsAll(values))
+                        assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
+                    break;
+                }
+            }
+            assertTrue(found, format("header name, %s, not found in %s", key, actual));
+        }
+    }
+
+    // ---
+
+    /* Main entry point for standalone testing of the main functional test. */
+    public static void main(String... args) throws Exception  {
+        Http1HeaderParserTest test = new Http1HeaderParserTest();
+        int count = 0;
+        for (Object[] objs : test.responses()) {
+            out.println("Testing " + count++ + ", " + objs[0]);
+            test.verifyHeaders((String) objs[0]);
+        }
+        for (Object[] objs : test.errors()) {
+            out.println("Testing " + count++ + ", " + objs[0]);
+            try {
+                test.errors((String) objs[0]);
+                throw new RuntimeException("Expected ProtocolException for " + objs[0]);
+            } catch (ProtocolException expected) { /* Ok */ }
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -24,6 +24,7 @@
 package jdk.incubator.http;
 
 import jdk.incubator.http.internal.websocket.RawChannel;
+import jdk.incubator.http.internal.websocket.WebSocketRequest;
 import org.testng.annotations.Test;
 
 import java.io.IOException;
@@ -83,6 +84,7 @@
             new TestServer(server).start();
 
             final RawChannel chan = channelOf(port);
+            print("RawChannel is %s", String.valueOf(chan));
             initialWriteStall.await();
 
             // It's very important not to forget the initial bytes, possibly
@@ -185,9 +187,21 @@
         URI uri = URI.create("http://127.0.0.1:" + port + "/");
         print("raw channel to %s", uri.toString());
         HttpRequest req = HttpRequest.newBuilder(uri).build();
-        HttpResponse<?> r = HttpClient.newHttpClient().send(req, discard(null));
-        r.body();
-        return ((HttpResponseImpl) r).rawChannel();
+        // Switch on isWebSocket flag to prevent the connection from
+        // being returned to the pool.
+        ((WebSocketRequest)req).isWebSocket(true);
+        HttpClient client = HttpClient.newHttpClient();
+        try {
+            HttpResponse<?> r = client.send(req, discard(null));
+            r.body();
+            return ((HttpResponseImpl) r).rawChannel();
+        } finally {
+           // Need to hold onto the client until the RawChannel is
+           // created. This would not be needed if we had created
+           // a WebSocket, but here we are fiddling directly
+           // with the internals of HttpResponseImpl!
+           java.lang.ref.Reference.reachabilityFence(client);
+        }
     }
 
     private class TestServer extends Thread { // Powered by Slowpokes
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ResponseHeadersTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ResponseHeadersTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -142,82 +142,14 @@
         throw new IOException("Status line not found");
     }
 
-    private static final class HttpConnectionStub extends HttpConnection {
-        public HttpConnectionStub() {
-            super(null, null);
-        }
-        @Override
-        public void connect() throws IOException, InterruptedException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        public CompletableFuture<Void> connectAsync() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        boolean connected() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        boolean isSecure() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        boolean isProxied() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        CompletableFuture<Void> whenReceivingResponse() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        SocketChannel channel() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        ConnectionPool.CacheKey cacheKey() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        long write(ByteBuffer[] buffers, int start, int number) throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        long write(ByteBuffer buffer) throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        void writeAsync(ByteBufferReference[] buffers) throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        void flushAsync() throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        public void close() {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        void shutdownInput() throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        void shutdownOutput() throws IOException {
-            throw new AssertionError("Bad test assumption: should not have reached here!");
-        }
-        @Override
-        protected ByteBuffer readImpl() throws IOException {
+    private static final class ByteBufferSupplierStub {
+        ByteBuffer read() {
             throw new AssertionError("Bad test assumption: should not have reached here!");
         }
     }
 
     public static HttpHeaders createResponseHeaders(ByteBuffer buffer)
         throws IOException{
-        return new ResponseHeaders(new HttpConnectionStub(), buffer);
+        return new ResponseHeaders(new ByteBufferSupplierStub()::read, buffer);
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SSLTubeTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,403 @@
+/*
+ * Copyright (c) 2017, 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.
+ */
+
+package jdk.incubator.http;
+
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Queue;
+import java.util.StringTokenizer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.SubmissionPublisher;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Test
+public class SSLTubeTest {
+
+    private static final long COUNTER = 600;
+    private static final int LONGS_PER_BUF = 800;
+    private static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF;
+
+    private static ByteBuffer getBuffer(long startingAt) {
+        ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8);
+        for (int j = 0; j < LONGS_PER_BUF; j++) {
+            buf.putLong(startingAt++);
+        }
+        buf.flip();
+        return buf;
+    }
+
+    @Test(timeOut = 30000)
+    public void run() throws IOException {
+        /* Start of wiring */
+        ExecutorService sslExecutor = Executors.newCachedThreadPool();
+        /* Emulates an echo server */
+        FlowTube server = new SSLTube(createSSLEngine(false),
+                                      sslExecutor,
+                                      new EchoTube(16));
+        FlowTube client = new SSLTube(createSSLEngine(true),
+                                      sslExecutor,
+                                      server);
+        SubmissionPublisher<List<ByteBuffer>> p =
+                new SubmissionPublisher<>(ForkJoinPool.commonPool(),
+                                          Integer.MAX_VALUE);
+        FlowTube.TubePublisher begin = p::subscribe;
+        CompletableFuture<Void> completion = new CompletableFuture<>();
+        EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion);
+        client.connectFlows(begin, end);
+        /* End of wiring */
+
+        long count = 0;
+        System.out.printf("Submitting %d buffer arrays\n", COUNTER);
+        System.out.printf("LoopCount should be %d\n", TOTAL_LONGS);
+        for (long i = 0; i < COUNTER; i++) {
+            ByteBuffer b = getBuffer(count);
+            count += LONGS_PER_BUF;
+            p.submit(List.of(b));
+        }
+        System.out.println("Finished submission. Waiting for loopback");
+        p.close();
+        try {
+            completion.join();
+            System.out.println("OK");
+        } finally {
+            sslExecutor.shutdownNow();
+        }
+    }
+
+    private static final class EchoTube implements FlowTube {
+
+        private final static Object EOF = new Object();
+        private final Executor executor = Executors.newSingleThreadExecutor();
+
+        private final Queue<Object> queue = new ConcurrentLinkedQueue<>();
+        private final int maxQueueSize;
+        private final SequentialScheduler processingScheduler =
+                new SequentialScheduler(createProcessingTask());
+
+        /* Writing into this tube */
+        private long unfulfilled;
+        private Flow.Subscription subscription;
+
+        /* Reading from this tube */
+        private final Demand demand = new Demand();
+        private final AtomicBoolean cancelled = new AtomicBoolean();
+        private Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+
+        private EchoTube(int maxBufferSize) {
+            if (maxBufferSize < 1)
+                throw new IllegalArgumentException();
+            this.maxQueueSize = maxBufferSize;
+        }
+
+        @Override
+        public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+            this.subscriber = subscriber;
+            this.subscriber.onSubscribe(new InternalSubscription());
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            unfulfilled = maxQueueSize;
+            (this.subscription = subscription).request(maxQueueSize);
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> item) {
+            if (--unfulfilled == (maxQueueSize / 2)) {
+                subscription.request(maxQueueSize - unfulfilled);
+                unfulfilled = maxQueueSize;
+            }
+            queue.add(item);
+            processingScheduler.deferOrSchedule(executor);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            queue.add(throwable);
+            processingScheduler.deferOrSchedule(executor);
+        }
+
+        @Override
+        public void onComplete() {
+            queue.add(EOF);
+            processingScheduler.deferOrSchedule(executor);
+        }
+
+        @Override
+        public boolean isFinished() {
+            return false;
+        }
+
+        private class InternalSubscription implements Flow.Subscription {
+
+            @Override
+            public void request(long n) {
+                if (n <= 0) {
+                    throw new InternalError();
+                }
+                demand.increase(n);
+                processingScheduler.runOrSchedule();
+            }
+
+            @Override
+            public void cancel() {
+                cancelled.set(true);
+            }
+        }
+
+        private SequentialScheduler.RestartableTask createProcessingTask() {
+            return new SequentialScheduler.CompleteRestartableTask() {
+
+                @Override
+                protected void run() {
+                    while (!cancelled.get()) {
+                        Object item = queue.peek();
+                        if (item == null)
+                            return;
+                        try {
+                            if (item instanceof List) {
+                                if (!demand.tryDecrement())
+                                    return;
+                                @SuppressWarnings("unchecked")
+                                List<ByteBuffer> bytes = (List<ByteBuffer>) item;
+                                subscriber.onNext(bytes);
+                            } else if (item instanceof Throwable) {
+                                cancelled.set(true);
+                                subscriber.onError((Throwable) item);
+                            } else if (item == EOF) {
+                                cancelled.set(true);
+                                subscriber.onComplete();
+                            } else {
+                                throw new InternalError(String.valueOf(item));
+                            }
+                        } finally {
+                            Object removed = queue.remove();
+                            assert removed == item;
+                        }
+                    }
+                }
+            };
+        }
+    }
+
+    /**
+     * The final subscriber which receives the decrypted looped-back data. Just
+     * needs to compare the data with what was sent. The given CF is either
+     * completed exceptionally with an error or normally on success.
+     */
+    private static class EndSubscriber implements FlowTube.TubeSubscriber {
+
+        private static final int REQUEST_WINDOW = 13;
+
+        private final long nbytes;
+        private final AtomicLong counter = new AtomicLong();
+        private final CompletableFuture<?> completion;
+        private volatile Flow.Subscription subscription;
+        private long unfulfilled;
+
+        EndSubscriber(long nbytes, CompletableFuture<?> completion) {
+            this.nbytes = nbytes;
+            this.completion = completion;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            this.subscription = subscription;
+            unfulfilled = REQUEST_WINDOW;
+            subscription.request(REQUEST_WINDOW);
+        }
+
+        public static String info(List<ByteBuffer> i) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("size: ").append(Integer.toString(i.size()));
+            int x = 0;
+            for (ByteBuffer b : i)
+                x += b.remaining();
+            sb.append(" bytes: ").append(x);
+            return sb.toString();
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> buffers) {
+            if (--unfulfilled == (REQUEST_WINDOW / 2)) {
+                subscription.request(REQUEST_WINDOW - unfulfilled);
+                unfulfilled = REQUEST_WINDOW;
+            }
+
+            long currval = counter.get();
+            if (currval % 500 == 0) {
+                System.out.println("End: " + currval);
+            }
+
+            for (ByteBuffer buf : buffers) {
+                while (buf.hasRemaining()) {
+                    long n = buf.getLong();
+                    if (currval > (SSLTubeTest.TOTAL_LONGS - 50)) {
+                        System.out.println("End: " + currval);
+                    }
+                    if (n != currval++) {
+                        System.out.println("ERROR at " + n + " != " + (currval - 1));
+                        completion.completeExceptionally(new RuntimeException("ERROR"));
+                        subscription.cancel();
+                        return;
+                    }
+                }
+            }
+
+            counter.set(currval);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            completion.completeExceptionally(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            long n = counter.get();
+            if (n != nbytes) {
+                System.out.printf("nbytes=%d n=%d\n", nbytes, n);
+                completion.completeExceptionally(new RuntimeException("ERROR AT END"));
+            } else {
+                System.out.println("DONE OK");
+                completion.complete(null);
+            }
+        }
+    }
+
+    private static SSLEngine createSSLEngine(boolean client) throws IOException {
+        SSLContext context = (new SimpleSSLContext()).get();
+        SSLEngine engine = context.createSSLEngine();
+        SSLParameters params = context.getSupportedSSLParameters();
+        params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl
+        if (client) {
+            params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2
+        } else {
+            params.setApplicationProtocols(new String[]{"proto2"}); // server will choose proto2
+        }
+        engine.setSSLParameters(params);
+        engine.setUseClientMode(client);
+        return engine;
+    }
+
+    /**
+     * Creates a simple usable SSLContext for SSLSocketFactory or a HttpsServer
+     * using either a given keystore or a default one in the test tree.
+     *
+     * Using this class with a security manager requires the following
+     * permissions to be granted:
+     *
+     * permission "java.util.PropertyPermission" "test.src.path", "read";
+     * permission java.io.FilePermission "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys",
+     * "read"; The exact path above depends on the location of the test.
+     */
+    private static class SimpleSSLContext {
+
+        private final SSLContext ssl;
+
+        /**
+         * Loads default keystore from SimpleSSLContext source directory
+         */
+        public SimpleSSLContext() throws IOException {
+            String paths = System.getProperty("test.src.path");
+            StringTokenizer st = new StringTokenizer(paths, File.pathSeparator);
+            boolean securityExceptions = false;
+            SSLContext sslContext = null;
+            while (st.hasMoreTokens()) {
+                String path = st.nextToken();
+                try {
+                    File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys");
+                    if (f.exists()) {
+                        try (FileInputStream fis = new FileInputStream(f)) {
+                            sslContext = init(fis);
+                            break;
+                        }
+                    }
+                } catch (SecurityException e) {
+                    // catch and ignore because permission only required
+                    // for one entry on path (at most)
+                    securityExceptions = true;
+                }
+            }
+            if (securityExceptions) {
+                System.err.println("SecurityExceptions thrown on loading testkeys");
+            }
+            ssl = sslContext;
+        }
+
+        private SSLContext init(InputStream i) throws IOException {
+            try {
+                char[] passphrase = "passphrase".toCharArray();
+                KeyStore ks = KeyStore.getInstance("JKS");
+                ks.load(i, passphrase);
+
+                KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+                kmf.init(ks, passphrase);
+
+                TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+                tmf.init(ks);
+
+                SSLContext ssl = SSLContext.getInstance("TLS");
+                ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+                return ssl;
+            } catch (KeyManagementException | KeyStoreException |
+                    UnrecoverableKeyException | CertificateException |
+                    NoSuchAlgorithmException e) {
+                throw new RuntimeException(e.getMessage());
+            }
+        }
+
+        public SSLContext get() {
+            return ssl;
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java	Sun Nov 05 17:05:57 2017 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -76,7 +76,7 @@
         }
     }
 
-    @Test(timeOut = 10000)
+    @Test
     public void test() throws Exception {
 
         try (ServerSocket server = new ServerSocket(0)) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/WrapperTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 2017, 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.
+ */
+
+package jdk.incubator.http;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+import org.testng.annotations.Test;
+import jdk.incubator.http.internal.common.SubscriberWrapper;
+
+@Test
+public class WrapperTest {
+    static final int LO_PRI = 1;
+    static final int HI_PRI = 2;
+    static final int NUM_HI_PRI = 240;
+    static final int BUFSIZE = 1016;
+    static final int BUFSIZE_INT = BUFSIZE/4;
+    static final int HI_PRI_FREQ = 40;
+
+    static final int TOTAL = 10000;
+    //static final int TOTAL = 500;
+
+    final SubmissionPublisher<List<ByteBuffer>> publisher;
+    final SubscriberWrapper sub1, sub2, sub3;
+    final ExecutorService executor = Executors.newCachedThreadPool();
+    volatile int hipricount = 0;
+
+    void errorHandler(Flow.Subscriber<? super List<ByteBuffer>> sub, Throwable t) {
+        System.err.printf("Exception from %s : %s\n", sub.toString(), t.toString());
+    }
+
+    public WrapperTest() {
+        publisher = new SubmissionPublisher<>(executor, 600,
+                (a, b) -> {
+                    errorHandler(a, b);
+                });
+
+        CompletableFuture<Void> notif = new CompletableFuture<>();
+        LastSubscriber ls = new LastSubscriber(notif);
+        sub1 = new Filter1(ls);
+        sub2 = new Filter2(sub1);
+        sub3 = new Filter2(sub2);
+    }
+
+    public class Filter2 extends SubscriberWrapper {
+        Filter2(SubscriberWrapper wrapper) {
+            super(wrapper);
+        }
+
+        // reverse the order of the bytes in each buffer
+        public void incoming(List<ByteBuffer> list, boolean complete) {
+            List<ByteBuffer> out = new LinkedList<>();
+            for (ByteBuffer inbuf : list) {
+                int size = inbuf.remaining();
+                ByteBuffer outbuf = ByteBuffer.allocate(size);
+                for (int i=size; i>0; i--) {
+                    byte b = inbuf.get(i-1);
+                    outbuf.put(b);
+                }
+                outbuf.flip();
+                out.add(outbuf);
+            }
+            if (complete) System.out.println("Filter2.complete");
+            outgoing(out, complete);
+        }
+
+        protected long windowUpdate(long currval) {
+            return currval == 0 ? 1 : 0;
+        }
+    }
+
+    volatile int filter1Calls = 0; // every third call we insert hi pri data
+
+    ByteBuffer getHiPri(int val) {
+        ByteBuffer buf = ByteBuffer.allocate(8);
+        buf.putInt(HI_PRI);
+        buf.putInt(val);
+        buf.flip();
+        return buf;
+    }
+
+    volatile int hiPriAdded = 0;
+
+    public class Filter1 extends SubscriberWrapper {
+        Filter1(Flow.Subscriber<List<ByteBuffer>> downstreamSubscriber)
+        {
+            super();
+            subscribe(downstreamSubscriber);
+        }
+
+        // Inserts up to NUM_HI_PRI hi priority buffers into flow
+        protected void incoming(List<ByteBuffer> in, boolean complete) {
+            if ((++filter1Calls % HI_PRI_FREQ) == 0 && (hiPriAdded++ < NUM_HI_PRI)) {
+                sub1.outgoing(getHiPri(hipricount++), false);
+            }
+            // pass data thru
+            if (complete) System.out.println("Filter1.complete");
+            outgoing(in, complete);
+        }
+
+        protected long windowUpdate(long currval) {
+            return currval == 0 ? 1 : 0;
+        }
+    }
+
+    /**
+     * Final subscriber in the chain. Compares the data sent by the original
+     * publisher.
+     */
+    static public class LastSubscriber implements Flow.Subscriber<List<ByteBuffer>> {
+        volatile Flow.Subscription subscription;
+        volatile int hipriCounter=0;
+        volatile int lopriCounter=0;
+        final CompletableFuture<Void> cf;
+
+        LastSubscriber(CompletableFuture<Void> cf) {
+            this.cf = cf;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            this.subscription = subscription;
+            subscription.request(50); // say
+        }
+
+        private void error(String...args) {
+            StringBuilder sb = new StringBuilder();
+            for (String s : args) {
+                sb.append(s);
+                sb.append(' ');
+            }
+            String msg = sb.toString();
+            System.out.println("Error: " + msg);
+            RuntimeException e = new RuntimeException(msg);
+            cf.completeExceptionally(e);
+            subscription.cancel(); // This is where we need a variant that include exception
+        }
+
+        private void check(ByteBuffer buf) {
+            int type = buf.getInt();
+            if (type == HI_PRI) {
+                // check next int is hi pri counter
+                int c = buf.getInt();
+                if (c != hipriCounter)
+                    error("hi pri counter", Integer.toString(c), Integer.toString(hipriCounter));
+                hipriCounter++;
+            } else {
+                while (buf.hasRemaining()) {
+                    if (buf.getInt() != lopriCounter)
+                        error("lo pri counter", Integer.toString(lopriCounter));
+                    lopriCounter++;
+                }
+            }
+        }
+
+        @Override
+        public void onNext(List<ByteBuffer> items) {
+            for (ByteBuffer item : items)
+                check(item);
+            subscription.request(1);
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            error(throwable.getMessage());
+        }
+
+        @Override
+        public void onComplete() {
+            if (hipriCounter != NUM_HI_PRI)
+                error("hi pri at end wrong", Integer.toString(hipriCounter), Integer.toString(NUM_HI_PRI));
+            else {
+                System.out.println("LastSubscriber.complete");
+                cf.complete(null); // success
+            }
+        }
+    }
+
+    List<ByteBuffer> getBuffer(int c) {
+        ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4);
+        buf.putInt(LO_PRI);
+        for (int i=0; i<BUFSIZE_INT; i++) {
+            buf.putInt(c++);
+        }
+        buf.flip();
+        return List.of(buf);
+    }
+
+    boolean errorTest = false;
+
+    @Test
+    public void run() throws InterruptedException {
+        try {
+            CompletableFuture<Void> completion = sub3.completion();
+            publisher.subscribe(sub3);
+            // now submit a load of data
+            int counter = 0;
+            for (int i = 0; i < TOTAL; i++) {
+                List<ByteBuffer> bufs = getBuffer(counter);
+                //if (i==2)
+                    //bufs.get(0).putInt(41, 1234); // error
+                counter += BUFSIZE_INT;
+                publisher.submit(bufs);
+                //if (i % 1000 == 0)
+                    //Thread.sleep(1000);
+                //if (i == 99) {
+                    //publisher.closeExceptionally(new RuntimeException("Test error"));
+                    //errorTest = true;
+                    //break;
+                //}
+            }
+            if (!errorTest) {
+                publisher.close();
+            }
+            System.out.println("Publisher completed");
+            completion.join();
+            System.out.println("Subscribers completed ok");
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    static void display(CompletableFuture<?> cf) {
+        System.out.print (cf);
+        if (!cf.isDone())
+            return;
+        try {
+            cf.join(); // wont block
+        } catch (Exception e) {
+            System.out.println(" " + e);
+        }
+    }
+
+/*
+    public static void main(String[] args) throws InterruptedException {
+        WrapperTest test = new WrapperTest();
+        test.run();
+    }
+*/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/common/DemandTest.java	Sun Nov 05 17:32:13 2017 +0000
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2017, 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.
+ */
+
+package jdk.incubator.http.internal.common;
+
+import org.testng.annotations.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+public class DemandTest {
+
+    @Test
+    public void test01() {
+        assertTrue(new Demand().isFulfilled());
+    }
+
+    @Test
+    public void test011() {
+        Demand d = new Demand();
+        d.increase(3);
+        d.decreaseAndGet(3);
+        assertTrue(d.isFulfilled());
+    }
+
+    @Test
+    public void test02() {
+        Demand d = new Demand();
+        d.increase(1);
+        assertFalse(d.isFulfilled());
+    }
+
+    @Test
+    public void test03() {
+        Demand d = new Demand();
+        d.increase(3);
+        assertEquals(d.decreaseAndGet(3), 3);
+    }
+
+    @Test
+    public void test04() {
+        Demand d = new Demand();
+        d.increase(3);
+        assertEquals(d.decreaseAndGet(5), 3);
+    }
+
+    @Test
+    public void test05() {
+        Demand d = new Demand();
+        d.increase(7);
+        assertEquals(d.decreaseAndGet(4), 4);
+    }
+
+    @Test
+    public void test06() {
+        Demand d = new Demand();
+        assertEquals(d.decreaseAndGet(3), 0);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void test07() {
+        Demand d = new Demand();
+        d.increase(0);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void test08() {
+        Demand d = new Demand();
+        d.increase(-1);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void test09() {
+        Demand d = new Demand();
+        d.increase(10);
+        d.decreaseAndGet(0);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void test10() {
+        Demand d = new Demand();
+        d.increase(13);
+        d.decreaseAndGet(-3);
+    }
+
+    @Test
+    public void test11() {
+        Demand d = new Demand();
+        d.increase(1);
+        assertTrue(d.tryDecrement());
+    }
+
+    @Test
+    public void test12() {
+        Demand d = new Demand();
+        d.increase(2);
+        assertTrue(d.tryDecrement());
+    }
+
+    @Test
+    public void test14() {
+        Demand d = new Demand();
+        assertFalse(d.tryDecrement());
+    }
+
+    @Test
+    public void test141() {
+        Demand d = new Demand();
+        d.increase(Long.MAX_VALUE);
+        assertFalse(d.isFulfilled());
+    }
+
+    @Test
+    public void test142() {
+        Demand d = new Demand();
+        d.increase(Long.MAX_VALUE);
+        d.increase(1);
+        assertFalse(d.isFulfilled());
+    }
+
+    @Test
+    public void test143() {
+        Demand d = new Demand();
+        d.increase(Long.MAX_VALUE);
+        d.increase(1);
+        assertFalse(d.isFulfilled());
+    }
+
+    @Test
+    public void test144() {
+        Demand d = new Demand();
+        d.increase(Long.MAX_VALUE);
+        d.increase(Long.MAX_VALUE);
+        d.decreaseAndGet(3);
+        d.decreaseAndGet(5);
+        assertFalse(d.isFulfilled());
+    }
+
+    @Test
+    public void test145() {
+        Demand d = new Demand();
+        d.increase(Long.MAX_VALUE);
+        d.decreaseAndGet(Long.MAX_VALUE);
+        assertTrue(d.isFulfilled());
+    }
+
+    @Test(invocationCount = 32)
+    public void test15() throws InterruptedException {
+        int N = Math.max(2, Runtime.getRuntime().availableProcessors() + 1);
+        int M = ((N + 1) * N) / 2; // 1 + 2 + 3 + ... N
+        Demand d = new Demand();
+        d.increase(M);
+        CyclicBarrier start = new CyclicBarrier(N);
+        CountDownLatch stop = new CountDownLatch(N);
+        AtomicReference<Throwable> error = new AtomicReference<>();
+        for (int i = 0; i < N; i++) {
+            int j = i + 1;
+            new Thread(() -> {
+                try {
+                    start.await();
+                } catch (Exception e) {
+                    error.compareAndSet(null, e);
+                }
+                try {
+                    assertEquals(d.decreaseAndGet(j), j);
+                } catch (Throwable t) {
+                    error.compareAndSet(null, t);
+                } finally {
+                    stop.countDown();
+                }
+            }).start();
+        }
+        stop.await();
+        assertTrue(d.isFulfilled());
+        assertEquals(error.get(), null);
+    }
+}