http-client-branch: (WebSocket) API changes + test; http-client-branch
authorprappo
Sun, 18 Mar 2018 14:31:36 +0000
branchhttp-client-branch
changeset 56318 2a96e88888b2
parent 56317 52a9f6b74e43
child 56319 bb9e25a8ea04
http-client-branch: (WebSocket) API changes + test;
src/java.net.http/share/classes/java/net/http/WebSocket.java
src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java
test/jdk/java/net/httpclient/websocket/ImmediateAbort.java
test/jdk/java/net/httpclient/websocket/SendTest.java
test/jdk/java/net/httpclient/websocket/Support.java
test/jdk/java/net/httpclient/websocket/WebSocketTest.java
--- a/src/java.net.http/share/classes/java/net/http/WebSocket.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/src/java.net.http/share/classes/java/net/http/WebSocket.java	Sun Mar 18 14:31:36 2018 +0000
@@ -26,7 +26,6 @@
 package java.net.http;
 
 import java.io.IOException;
-import java.net.ProtocolException;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.time.Duration;
@@ -67,12 +66,10 @@
  * {@code WebSocket} will not pass {@code null} arguments to methods of
  * {@code Listener}.
  *
- * @implSpec Methods of {@code WebSocket} are failure-atomic in respect to
- * {@code NullPointerException}, {@code IllegalArgumentException} and
- * {@code IllegalStateException}. That is, if a method throws said exception, or
- * a returned {@code CompletableFuture} completes exceptionally with said
- * exception, the {@code WebSocket} will behave as if the method has not been
- * invoked at all.
+ * <p> The state of a {@code WebSocket} is not changed by the invocations that
+ * throw or return a {@code CompletableFuture} that completes with one of the
+ * {@code NullPointerException}, {@code IllegalArgumentException},
+ * {@code IllegalStateException} exceptions.
  *
  * <p> A {@code WebSocket} invokes methods of its listener in a thread-safe
  * manner.
@@ -211,23 +208,29 @@
     /**
      * The receiving interface of {@code WebSocket}.
      *
-     * <p> A {@code WebSocket} invokes methods on its listener when it receives
-     * messages or encounters events. The invoking {@code WebSocket} is passed
-     * as an argument to {@code Listener}'s methods. A {@code WebSocket} invokes
-     * methods on its listener in a thread-safe manner.
+     * <p> A {@code WebSocket} invokes methods on the associated listener when
+     * it receives messages or encounters events. A {@code WebSocket} invokes
+     * methods on the listener in a thread-safe manner.
      *
      * <p> Messages received by the {@code Listener} conform to the WebSocket
-     * Protocol, otherwise {@code onError} with a {@link ProtocolException} is
-     * invoked.
-     *
-     * <p> Unless otherwise stated if a listener's method throws an exception or
-     * a {@code CompletionStage} returned from a method completes exceptionally,
+     * Protocol, otherwise {@code onError} with a {@link IOException} is invoked.
+     * Any {@code IOException} raised by {@code WebSocket} will result in an
+     * invocation of {@code onError} with that exception. Unless otherwise
+     * stated if a listener's method throws an exception or a
+     * {@code CompletionStage} returned from a method completes exceptionally,
      * the {@code WebSocket} will invoke {@code onError} with this exception.
      *
      * <p> If a listener's method returns {@code null} rather than a
      * {@code CompletionStage}, {@code WebSocket} will behave as if the listener
      * returned a {@code CompletionStage} that is already completed normally.
      *
+     * @apiNote Methods of {@code Listener} have a {@code WebSocket} parameter
+     * which holds an invoking {@code WebSocket} at runtime. A careful attention
+     * is required if a listener is associated with more than a single
+     * {@code WebSocket}. In this case invocations related to different
+     * instances of {@code WebSocket} may not be ordered and may even happen
+     * concurrently.
+     *
      * @since 11
      */
     interface Listener {
@@ -402,7 +405,8 @@
         }
 
         /**
-         * A Close message has been received.
+         * Receives a Close message indicating the {@code WebSocket}'s input has
+         * been closed.
          *
          * <p> This is the last invocation from the {@code WebSocket}. By the
          * time this invocation begins the {@code WebSocket}'s input will have
@@ -415,21 +419,30 @@
          * {@code 1000 <= code <= 65535}. The {@code reason} is a string which
          * has an UTF-8 representation not longer than {@code 123} bytes.
          *
-         * <p> Return a {@code CompletionStage} that will be used by the
-         * {@code WebSocket} as a signal that it may close the output. The
-         * {@code WebSocket} will close the output at the earliest of completion
-         * of the returned {@code CompletionStage} or invoking a
-         * {@link WebSocket#sendClose(int, String) sendClose} method.
-         *
-         * <p> If an exception is thrown from this method or a
-         * {@code CompletionStage} returned from it completes exceptionally,
-         * the resulting behaviour is undefined.
+         * <p> If the {@code WebSocket}'s output is not already closed, the
+         * {@code CompletionStage} returned by this method will be used as an
+         * indication that the {@code WebSocket}'s output may be closed. The
+         * {@code WebSocket} will close its output at the earliest of completion
+         * of the returned {@code CompletionStage} or invoking either of the
+         * {@code sendClose} or {@code abort} methods.
          *
          * @apiNote Returning a {@code CompletionStage} that never completes,
-         * effectively disables the automatic closure of the output.
+         * effectively disables the reciprocating closure of the output.
+         *
+         * <p> To specify a custom closure code and/or reason code the sendClose
+         * may be invoked from inside onClose call:
+         * <pre>{@code
+         *  public CompletionStage<?> onClose(WebSocket webSocket,
+         *                             int statusCode,
+         *                             String reason) {
+         *      webSocket.sendClose(CUSTOM_STATUS_CODE, CUSTOM_REASON);
+         *      return new CompletableFuture<Void>();
+         *  }
+         * }</pre>
          *
          * @implSpec The default implementation of this method returns
-         * {@code null}, signaling that the output may be closed.
+         * {@code null}, indicating that the output should be closed
+         * immediately.
          *
          * @param webSocket
          *         the WebSocket on which the message has been received
@@ -449,7 +462,7 @@
         }
 
         /**
-         * An unrecoverable error has occurred.
+         * An error has occurred.
          *
          * <p> This is the last invocation from the {@code WebSocket}. By the
          * time this invocation begins both {@code WebSocket}'s input and output
@@ -500,10 +513,8 @@
     /**
      * Sends a Text message with characters from the given {@code CharSequence}.
      *
-     * <p> To send a Text message invoke this method only after the previous
-     * Text or Binary message has been sent. The character sequence must not be
-     * modified until the {@code CompletableFuture} returned from this method
-     * has completed.
+     * <p> The character sequence must not be modified until the
+     * {@code CompletableFuture} returned from this method has completed.
      *
      * <p> A {@code CompletableFuture} returned from this method can
      * complete exceptionally with:
@@ -513,7 +524,7 @@
      *          or if a previous Binary message has been sent with
      *          {@code isLast == false}
      * <li> {@link IOException} -
-     *          if an I/O error occurs
+     *          if an I/O error occurs, or if the output is closed
      * </ul>
      *
      * @implNote If a partial or malformed UTF-16 sequence is passed to this
@@ -534,11 +545,10 @@
     /**
      * Sends a Binary message with bytes from the given {@code ByteBuffer}.
      *
-     * <p> To send a Binary message invoke this method only after the previous
-     * Text or Binary message has been sent. The message consists of bytes from
-     * the buffer's position to its limit. Upon normal completion of a
-     * {@code CompletableFuture} returned from this method the buffer will have
-     * no remaining bytes. The buffer must not be accessed until after that.
+     * <p> The message consists of bytes from the buffer's position to its
+     * limit. Upon normal completion of a {@code CompletableFuture} returned
+     * from this method the buffer will have no remaining bytes. The buffer must
+     * not be accessed until after that.
      *
      * <p> The {@code CompletableFuture} returned from this method can
      * complete exceptionally with:
@@ -548,7 +558,7 @@
      *          or if a previous Text message has been sent with
      *              {@code isLast == false}
      * <li> {@link IOException} -
-     *          if an I/O error occurs
+     *          if an I/O error occurs, or if the output is closed
      * </ul>
      *
      * @param message
@@ -573,10 +583,12 @@
      * <p> The {@code CompletableFuture} returned from this method can
      * complete exceptionally with:
      * <ul>
+     * <li> {@link IllegalStateException} -
+     *          if the previous Ping or Pong message has not been sent yet
      * <li> {@link IllegalArgumentException} -
      *          if the message is too long
      * <li> {@link IOException} -
-     *          if an I/O error occurs
+     *          if an I/O error occurs, or if the output is closed
      * </ul>
      *
      * @param message
@@ -598,10 +610,12 @@
      * <p> The {@code CompletableFuture} returned from this method can
      * complete exceptionally with:
      * <ul>
+     * <li> {@link IllegalStateException} -
+     *          if the previous Ping or Pong message has not been sent yet
      * <li> {@link IllegalArgumentException} -
      *          if the message is too long
      * <li> {@link IOException} -
-     *          if an I/O error occurs
+     *          if an I/O error occurs, or if the output is closed
      * </ul>
      *
      * @param message
@@ -613,8 +627,8 @@
     CompletableFuture<WebSocket> sendPong(ByteBuffer message);
 
     /**
-     * Sends a Close message with the given status code and the reason,
-     * initiating an orderly closure.
+     * Initiates an orderly closure of this {@code WebSocket}'s output by
+     * sending a Close message with the given status code and the reason.
      *
      * <p> The {@code statusCode} is an integer from the range
      * {@code 1000 <= code <= 4999}. Status codes {@code 1002}, {@code 1003},
@@ -623,29 +637,33 @@
      * status codes is implementation-specific. The {@code reason} is a string
      * that has an UTF-8 representation not longer than {@code 123} bytes.
      *
-     * <p> Use the provided integer constant {@link #NORMAL_CLOSURE} as a status
-     * code and an empty string as a reason in a typical case.
-     *
      * <p> A {@code CompletableFuture} returned from this method can
      * complete exceptionally with:
      * <ul>
      * <li> {@link IllegalArgumentException} -
      *          if {@code statusCode} is illegal
      * <li> {@link IOException} -
-     *          if an I/O error occurs
+     *          if an I/O error occurs, or if the output is closed
      * </ul>
      *
-     * <p> By the time the {@code CompletableFuture} returned from this method
-     * completes normally, the output will have been closed.
+     * <p> Unless the {@code CompletableFuture} returned from this method
+     * completes with {@code IllegalArgumentException}, or the method throws
+     * {@code NullPointerException}, the output will be closed.
+     *
+     * <p> If not already closed, the input remains open until it is
+     * {@linkplain Listener#onClose(WebSocket, int, String) closed} by the server,
+     * or {@code abort} is invoked, or an
+     * {@linkplain Listener#onError(WebSocket, Throwable) error} occurs.
      *
-     * @implSpec An endpoint sending a Close message might not receive a
-     * complementing Close message in a timely manner for a variety of reasons.
-     * The {@code WebSocket} implementation is responsible for providing a
-     * closure mechanism that guarantees that once {@code sendClose} method has
-     * been invoked the {@code WebSocket} will close regardless of whether or
-     * not a Close frame has been received and without further intervention from
-     * the user of this API. Method {@code sendClose} is designed to be,
-     * possibly, the last call from the user of this API.
+     * @apiNote Use the provided integer constant {@link #NORMAL_CLOSURE} as a
+     * status code and an empty string as a reason in a typical case
+     * <pre>{@code
+     *     CompletableFuture<WebSocket> webSocket = ...
+     *     webSocket.thenCompose(ws -> ws.sendText("Hello, ", false))
+     *              .thenCompose(ws -> ws.sendText("world!", true))
+     *              .thenCompose(ws -> ws.sendClose(WebSocket.NORMAL_CLOSURE, ""))
+     *              .join();
+     * }</pre>
      *
      * @param statusCode
      *         the status code
@@ -681,8 +699,7 @@
     String getSubprotocol();
 
     /**
-     * Tells whether or not this {@code WebSocket} is permanently closed
-     * for sending messages.
+     * Tells whether this {@code WebSocket}'s output is closed.
      *
      * <p> If this method returns {@code true}, subsequent invocations will also
      * return {@code true}.
@@ -692,8 +709,7 @@
     boolean isOutputClosed();
 
     /**
-     * Tells whether or not this {@code WebSocket} is permanently closed
-     * for receiving messages.
+     * Tells whether this {@code WebSocket}'s input is closed.
      *
      * <p> If this method returns {@code true}, subsequent invocations will also
      * return {@code true}.
@@ -703,15 +719,11 @@
     boolean isInputClosed();
 
     /**
-     * Closes this {@code WebSocket} abruptly.
+     * Closes this {@code WebSocket}'s input and output abruptly.
      *
      * <p> When this method returns both the input and the output will have been
-     * closed. Subsequent invocations will have no effect.
-     *
-     * @apiNote Depending on its implementation, the state (for example, whether
-     * or not a message is being transferred at the moment) and possible errors
-     * while releasing associated resources, this {@code WebSocket} may invoke
-     * its listener's {@code onError}.
+     * closed. Any pending send operations will fail with {@code IOException}.
+     * Subsequent invocations of {@code abort()} will have no effect.
      */
     void abort();
 }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java	Sun Mar 18 14:31:36 2018 +0000
@@ -115,7 +115,8 @@
     private final String subprotocol;
     private final Listener listener;
 
-    private final AtomicBoolean outstandingSend = new AtomicBoolean();
+    private final AtomicBoolean pendingTextOrBinary = new AtomicBoolean();
+    private final AtomicBoolean pendingPingOrPong = new AtomicBoolean();
     private final Transport transport;
     private final SequentialScheduler receiveScheduler
             = new SequentialScheduler(new ReceiveTask());
@@ -198,11 +199,11 @@
                       id, message.length(), isLast);
         }
         CompletableFuture<WebSocket> result;
-        if (!outstandingSend.compareAndSet(false, true)) {
+        if (!setPendingTextOrBinary()) {
             result = failedFuture(new IllegalStateException("Send pending"));
         } else {
             result = transport.sendText(message, isLast, this,
-                                        (r, e) -> outstandingSend.set(false));
+                                        (r, e) -> clearPendingTextOrBinary());
         }
         debug.log(Level.DEBUG, "exit send text %s returned %s", id, result);
 
@@ -220,16 +221,24 @@
                       id, message, isLast);
         }
         CompletableFuture<WebSocket> result;
-        if (!outstandingSend.compareAndSet(false, true)) {
+        if (!setPendingTextOrBinary()) {
             result = failedFuture(new IllegalStateException("Send pending"));
         } else {
             result = transport.sendBinary(message, isLast, this,
-                                          (r, e) -> outstandingSend.set(false));
+                                          (r, e) -> clearPendingTextOrBinary());
         }
         debug.log(Level.DEBUG, "exit send binary %s returned %s", id, result);
         return replaceNull(result);
     }
 
+    private void clearPendingTextOrBinary() {
+        pendingTextOrBinary.set(false);
+    }
+
+    private boolean setPendingTextOrBinary() {
+        return pendingTextOrBinary.compareAndSet(false, true);
+    }
+
     private CompletableFuture<WebSocket> replaceNull(
             CompletableFuture<WebSocket> cf)
     {
@@ -248,8 +257,13 @@
             id = sendCounter.incrementAndGet();
             debug.log(Level.DEBUG, "enter send ping %s payload=%s", id, message);
         }
-        CompletableFuture<WebSocket> result = transport.sendPing(message, this,
-                                                                 (r, e) -> { });
+        CompletableFuture<WebSocket> result;
+        if (!setPendingPingOrPong()) {
+            result = failedFuture(new IllegalStateException("Send pending"));
+        } else {
+            result = transport.sendPing(message, this,
+                                        (r, e) -> clearPendingPingOrPong());
+        }
         debug.log(Level.DEBUG, "exit send ping %s returned %s", id, result);
         return replaceNull(result);
     }
@@ -262,12 +276,25 @@
             id = sendCounter.incrementAndGet();
             debug.log(Level.DEBUG, "enter send pong %s payload=%s", id, message);
         }
-        CompletableFuture<WebSocket> result = transport.sendPong(message, this,
-                                                                 (r, e) -> { });
+        CompletableFuture<WebSocket> result;
+        if (!setPendingPingOrPong()) {
+            result = failedFuture(new IllegalStateException("Send pending"));
+        } else {
+            result =  transport.sendPong(message, this,
+                                         (r, e) -> clearPendingPingOrPong());
+        }
         debug.log(Level.DEBUG, "exit send pong %s returned %s", id, result);
         return replaceNull(result);
     }
 
+    private boolean setPendingPingOrPong() {
+        return pendingPingOrPong.compareAndSet(false, true);
+    }
+
+    private void clearPendingPingOrPong() {
+        pendingPingOrPong.set(false);
+    }
+
     @Override
     public CompletableFuture<WebSocket> sendClose(int statusCode,
                                                   String reason) {
--- a/test/jdk/java/net/httpclient/websocket/ImmediateAbort.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/test/jdk/java/net/httpclient/websocket/ImmediateAbort.java	Sun Mar 18 14:31:36 2018 +0000
@@ -29,16 +29,16 @@
  *       ImmediateAbort
  */
 
+import org.testng.annotations.Test;
+
 import java.io.IOException;
 import java.net.http.WebSocket;
 import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.Arrays;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import org.testng.annotations.Test;
+
 import static java.net.http.HttpClient.newHttpClient;
 import static java.net.http.WebSocket.NORMAL_CLOSURE;
 import static org.testng.Assert.assertEquals;
@@ -57,7 +57,7 @@
      */
     @Test
     public void immediateAbort() throws Exception {
-        try (DummyWebSocketServer server = serverWithCannedData(0x81, 0x00, 0x88, 0x00)) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(0x81, 0x00, 0x88, 0x00)) {
             server.open();
             CompletableFuture<Void> messageReceived = new CompletableFuture<>();
             WebSocket.Listener listener = new WebSocket.Listener() {
@@ -113,8 +113,8 @@
             for (int i = 0; i < 3; i++) {
                 System.out.printf("iteration #%s%n", i);
                 // after the first abort() each consecutive one must be a no-op,
-                // moreover, query methods should continue to return consistent,
-                // permanent values
+                // moreover, query methods should continue to return consistent
+                // values
                 for (int j = 0; j < 3; j++) {
                     System.out.printf("abort #%s%n", j);
                     ws.abort();
@@ -148,13 +148,13 @@
             }
             for (int i = 0; i < 3; i++) {
                 System.out.printf("send #%s%n", i);
-                assertFails(IOE, ws.sendText("text!", false));
-                assertFails(IOE, ws.sendText("text!", true));
-                assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), false));
-                assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), true));
-                assertFails(IOE, ws.sendPing(ByteBuffer.allocate(16)));
-                assertFails(IOE, ws.sendPong(ByteBuffer.allocate(16)));
-                assertFails(IOE, ws.sendClose(NORMAL_CLOSURE, "a reason"));
+                Support.assertFails(IOE, ws.sendText("text!", false));
+                Support.assertFails(IOE, ws.sendText("text!", true));
+                Support.assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), false));
+                Support.assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), true));
+                Support.assertFails(IOE, ws.sendPing(ByteBuffer.allocate(16)));
+                Support.assertFails(IOE, ws.sendPong(ByteBuffer.allocate(16)));
+                Support.assertFails(IOE, ws.sendClose(NORMAL_CLOSURE, "a reason"));
                 assertThrows(NPE, () -> ws.sendText(null, false));
                 assertThrows(NPE, () -> ws.sendText(null, true));
                 assertThrows(NPE, () -> ws.sendBinary(null, false));
@@ -165,29 +165,4 @@
             }
         }
     }
-
-    private static void assertFails(Class<? extends Throwable> clazz,
-                                    CompletionStage<?> stage) {
-        Support.assertCompletesExceptionally(clazz, stage);
-    }
-
-    private static DummyWebSocketServer serverWithCannedData(int... data) {
-        byte[] copy = new byte[data.length];
-        for (int i = 0; i < data.length; i++) {
-            copy[i] = (byte) data[i];
-        }
-        return serverWithCannedData(copy);
-    }
-
-    private static DummyWebSocketServer serverWithCannedData(byte... data) {
-        byte[] copy = Arrays.copyOf(data, data.length);
-        return new DummyWebSocketServer() {
-            @Override
-            protected void serve(SocketChannel channel) throws IOException {
-                ByteBuffer closeMessage = ByteBuffer.wrap(copy);
-                channel.write(closeMessage);
-                super.serve(channel);
-            }
-        };
-    }
 }
--- a/test/jdk/java/net/httpclient/websocket/SendTest.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/test/jdk/java/net/httpclient/websocket/SendTest.java	Sun Mar 18 14:31:36 2018 +0000
@@ -29,17 +29,15 @@
  *       SendTest
  */
 
+import org.testng.annotations.Test;
+
 import java.io.IOException;
 import java.net.http.WebSocket;
 import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import org.testng.annotations.Test;
+
 import static java.net.http.HttpClient.newHttpClient;
 import static java.net.http.WebSocket.NORMAL_CLOSURE;
 import static org.testng.Assert.assertEquals;
@@ -51,24 +49,6 @@
 
     private static final Class<NullPointerException> NPE = NullPointerException.class;
 
-    /* shortcut */
-    private static void assertFails(Class<? extends Throwable> clazz,
-                                    CompletionStage<?> stage) {
-        Support.assertCompletesExceptionally(clazz, stage);
-    }
-
-    private static DummyWebSocketServer serverWithCannedData(byte... data) {
-        byte[] copy = Arrays.copyOf(data, data.length);
-        return new DummyWebSocketServer() {
-            @Override
-            protected void serve(SocketChannel channel) throws IOException {
-                ByteBuffer closeMessage = ByteBuffer.wrap(copy);
-                channel.write(closeMessage);
-                super.serve(channel);
-            }
-        };
-    }
-
     @Test
     public void sendMethodsThrowNPE() throws IOException {
         try (DummyWebSocketServer server = new DummyWebSocketServer()) {
@@ -119,7 +99,7 @@
 
     @Test
     public void sendClosePending() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
@@ -151,27 +131,9 @@
         }
     }
 
-    /*
-     * This server does not read from the wire, allowing its client to fill up
-     * their send buffer. Used to test scenarios with outstanding send
-     * operations.
-     */
-    private static DummyWebSocketServer notReadingServer() {
-        return new DummyWebSocketServer() {
-            @Override
-            protected void serve(SocketChannel channel) throws IOException {
-                try {
-                    Thread.sleep(Long.MAX_VALUE);
-                } catch (InterruptedException e) {
-                    throw new IOException(e);
-                }
-            }
-        };
-    }
-
     @Test
     public void abortPendingSendBinary() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
@@ -196,19 +158,19 @@
             ws.abort();
             assertTrue(ws.isOutputClosed());
             assertTrue(ws.isInputClosed());
-            assertFails(IOException.class, cf);
+            Support.assertFails(IOException.class, cf);
         }
     }
 
     @Test
     public void abortPendingSendText() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
                     .join();
-            String data = stringWith2NBytes(32768);
+            String data = Support.stringWith2NBytes(32768);
             CompletableFuture<WebSocket> cf = null;
             for (int i = 0; ; i++) {  // fill up the send buffer
                 System.out.printf("begin cycle #%s at %s%n",
@@ -226,38 +188,19 @@
             ws.abort();
             assertTrue(ws.isOutputClosed());
             assertTrue(ws.isInputClosed());
-            assertFails(IOException.class, cf);
+            Support.assertFails(IOException.class, cf);
         }
     }
 
-    private static String stringWith2NBytes(int n) {
-        // -- Russian Alphabet (33 characters, 2 bytes per char) --
-        char[] abc = {
-                0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
-                0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
-                0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
-                0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
-                0x042F,
-        };
-        // repeat cyclically
-        StringBuilder sb = new StringBuilder(n);
-        for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
-            sb.append(abc[j]);
-        }
-        String s = sb.toString();
-        assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
-        return s;
-    }
-
     @Test
     public void sendCloseTimeout() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
                     .join();
-            String data = stringWith2NBytes(32768);
+            String data = Support.stringWith2NBytes(32768);
             CompletableFuture<WebSocket> cf = null;
             for (int i = 0; ; i++) {  // fill up the send buffer
                 System.out.printf("begin cycle #%s at %s%n",
@@ -273,7 +216,7 @@
                 }
             }
             long before = System.currentTimeMillis();
-            assertFails(IOException.class,
+            Support.assertFails(IOException.class,
                     ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"));
             long after = System.currentTimeMillis();
             // default timeout should be 30 seconds
@@ -282,7 +225,7 @@
             assertTrue(elapsed >= 29_000, String.valueOf(elapsed));
             assertTrue(ws.isOutputClosed());
             assertTrue(ws.isInputClosed());
-            assertFails(IOException.class, cf);
+            Support.assertFails(IOException.class, cf);
         }
     }
 }
--- a/test/jdk/java/net/httpclient/websocket/Support.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/test/jdk/java/net/httpclient/websocket/Support.java	Sun Mar 18 14:31:36 2018 +0000
@@ -21,10 +21,16 @@
  * questions.
  */
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import static org.testng.Assert.assertThrows;
 
@@ -32,6 +38,11 @@
 
     private Support() { }
 
+    public static void assertFails(Class<? extends Throwable> clazz,
+                                    CompletionStage<?> stage) {
+        Support.assertCompletesExceptionally(clazz, stage);
+    }
+
     public static void assertCompletesExceptionally(Class<? extends Throwable> clazz,
                                                     CompletionStage<?> stage) {
         CompletableFuture<?> cf =
@@ -45,6 +56,18 @@
         });
     }
 
+    public static void assertHangs(CompletionStage<?> stage) {
+        Support.assertDoesNotCompleteWithin(5, TimeUnit.SECONDS, stage);
+    }
+
+    public static void assertDoesNotCompleteWithin(long timeout,
+                                                   TimeUnit unit,
+                                                   CompletionStage<?> stage) {
+        CompletableFuture<?> cf =
+                CompletableFuture.completedFuture(null).thenCompose(x -> stage);
+        assertThrows(TimeoutException.class, () -> cf.get(timeout, unit));
+    }
+
     public static ByteBuffer fullCopy(ByteBuffer src) {
         ByteBuffer copy = ByteBuffer.allocate(src.capacity());
         int p = src.position();
@@ -54,4 +77,75 @@
         src.position(p).limit(l);
         return copy;
     }
+
+    public static DummyWebSocketServer serverWithCannedData(int... data) {
+        byte[] copy = new byte[data.length];
+        for (int i = 0; i < data.length; i++) {
+            copy[i] = (byte) data[i];
+        }
+        return serverWithCannedData(copy);
+    }
+
+    public static DummyWebSocketServer serverWithCannedData(byte... data) {
+        byte[] copy = Arrays.copyOf(data, data.length);
+        return new DummyWebSocketServer() {
+            @Override
+            protected void serve(SocketChannel channel) throws IOException {
+                ByteBuffer closeMessage = ByteBuffer.wrap(copy);
+                channel.write(closeMessage);
+                super.serve(channel);
+            }
+        };
+    }
+
+    /*
+     * This server does not read from the wire, allowing its client to fill up
+     * their send buffer. Used to test scenarios with outstanding send
+     * operations.
+     */
+    public static DummyWebSocketServer notReadingServer() {
+        return new DummyWebSocketServer() {
+            @Override
+            protected void serve(SocketChannel channel) throws IOException {
+                try {
+                    Thread.sleep(Long.MAX_VALUE);
+                } catch (InterruptedException e) {
+                    throw new IOException(e);
+                }
+            }
+        };
+    }
+
+    public static String stringWith2NBytes(int n) {
+        // -- Russian Alphabet (33 characters, 2 bytes per char) --
+        char[] abc = {
+                0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
+                0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
+                0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
+                0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
+                0x042F,
+        };
+        // repeat cyclically
+        StringBuilder sb = new StringBuilder(n);
+        for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
+            sb.append(abc[j]);
+        }
+        String s = sb.toString();
+        assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
+        return s;
+    }
+
+    public static String malformedString() {
+        return new String(new char[]{0xDC00, 0xD800});
+    }
+
+    public static String incompleteString() {
+        return new String(new char[]{0xD800});
+    }
+
+    public static String stringWithNBytes(int n) {
+        char[] chars = new char[n];
+        Arrays.fill(chars, 'A');
+        return new String(chars);
+    }
 }
--- a/test/jdk/java/net/httpclient/websocket/WebSocketTest.java	Sat Mar 17 18:01:01 2018 +0000
+++ b/test/jdk/java/net/httpclient/websocket/WebSocketTest.java	Sun Mar 18 14:31:36 2018 +0000
@@ -24,27 +24,27 @@
 /*
  * @test
  * @build DummyWebSocketServer
- * @run testng/othervm
+ * @run testng/othervm/timeout=600
  *      -Djdk.internal.httpclient.websocket.debug=true
  *       WebSocketTest
  */
 
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
 import java.io.IOException;
 import java.net.http.WebSocket;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
-import java.nio.channels.SocketChannel;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
+
 import static java.net.http.HttpClient.newHttpClient;
 import static java.net.http.WebSocket.NORMAL_CLOSURE;
 import static org.testng.Assert.assertEquals;
@@ -65,65 +65,8 @@
         Support.assertCompletesExceptionally(clazz, stage);
     }
 
-    private static DummyWebSocketServer serverWithCannedData(int... data) {
-        byte[] copy = new byte[data.length];
-        for (int i = 0; i < data.length; i++) {
-            copy[i] = (byte) data[i];
-        }
-        return serverWithCannedData(copy);
-    }
-
-    private static DummyWebSocketServer serverWithCannedData(byte... data) {
-        byte[] copy = Arrays.copyOf(data, data.length);
-        return new DummyWebSocketServer() {
-            @Override
-            protected void serve(SocketChannel channel) throws IOException {
-                ByteBuffer closeMessage = ByteBuffer.wrap(copy);
-                channel.write(closeMessage);
-                super.serve(channel);
-            }
-        };
-    }
-
-    /*
-     * This server does not read from the wire, allowing its client to fill up
-     * their send buffer. Used to test scenarios with outstanding send
-     * operations.
-     */
-    private static DummyWebSocketServer notReadingServer() {
-        return new DummyWebSocketServer() {
-            @Override
-            protected void serve(SocketChannel channel) throws IOException {
-                try {
-                    Thread.sleep(Long.MAX_VALUE);
-                } catch (InterruptedException e) {
-                    throw new IOException(e);
-                }
-            }
-        };
-    }
-
-    private static String stringWith2NBytes(int n) {
-        // -- Russian Alphabet (33 characters, 2 bytes per char) --
-        char[] abc = {
-                0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
-                0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
-                0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
-                0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
-                0x042F,
-        };
-        // repeat cyclically
-        StringBuilder sb = new StringBuilder(n);
-        for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
-            sb.append(abc[j]);
-        }
-        String s = sb.toString();
-        assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
-        return s;
-    }
-
     @Test
-    public void testIllegalArgument() throws IOException {
+    public void illegalArgument() throws IOException {
         try (DummyWebSocketServer server = new DummyWebSocketServer()) {
             server.open();
             WebSocket ws = newHttpClient()
@@ -143,19 +86,19 @@
             assertFails(IAE, ws.sendPong(ByteBuffer.allocate(129)));
             assertFails(IAE, ws.sendPong(ByteBuffer.allocate(256)));
 
-            assertFails(IOE, ws.sendText(incompleteString(), true));
-            assertFails(IOE, ws.sendText(incompleteString(), false));
-            assertFails(IOE, ws.sendText(malformedString(), true));
-            assertFails(IOE, ws.sendText(malformedString(), false));
+            assertFails(IOE, ws.sendText(Support.incompleteString(), true));
+            assertFails(IOE, ws.sendText(Support.incompleteString(), false));
+            assertFails(IOE, ws.sendText(Support.malformedString(), true));
+            assertFails(IOE, ws.sendText(Support.malformedString(), false));
 
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWithNBytes(124)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWithNBytes(125)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWithNBytes(128)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWithNBytes(256)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWithNBytes(257)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, stringWith2NBytes((123 / 2) + 1)));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, malformedString()));
-            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, incompleteString()));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(124)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(125)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(128)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(256)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(257)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.stringWith2NBytes((123 / 2) + 1)));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.malformedString()));
+            assertFails(IAE, ws.sendClose(NORMAL_CLOSURE, Support.incompleteString()));
 
             assertFails(IAE, ws.sendClose(-2, "a reason"));
             assertFails(IAE, ws.sendClose(-1, "a reason"));
@@ -187,23 +130,98 @@
         }
     }
 
-    private static String malformedString() {
-        return new String(new char[]{0xDC00, 0xD800});
-    }
+    @Test(dataProvider = "booleans")
+    public void pendingTextPingClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
 
-    private static String incompleteString() {
-        return new String(new char[]{0xD800});
+            CharBuffer data = CharBuffer.allocate(65536);
+            CompletableFuture<WebSocket> cfText;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.printf("begin cycle #%s at %s%n",
+                                  i, System.currentTimeMillis());
+                cfText = ws.sendText(data, last);
+                try {
+                    cfText.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                } finally {
+                    System.out.printf("end cycle #%s at %s%n",
+                                      i, System.currentTimeMillis());
+                }
+            }
+            assertFails(ISE, ws.sendText("", true));
+            assertFails(ISE, ws.sendText("", false));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), true));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), false));
+            CompletableFuture<WebSocket> cfPing = ws.sendPing(ByteBuffer.allocate(125));
+            assertHangs(cfPing);
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfClose = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfText);
+            assertFails(IOE, cfPing);
+            assertFails(IOE, cfClose);
+        }
     }
 
-    private static String stringWithNBytes(int n) {
-        char[] chars = new char[n];
-        Arrays.fill(chars, 'A');
-        return new String(chars);
+    /* shortcut */
+    public static void assertHangs(CompletionStage<?> stage) {
+        Support.assertHangs(stage);
     }
 
-    @Test
-    public void outstanding1() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+    @Test(dataProvider = "booleans")
+    public void pendingTextPongClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+
+            CharBuffer data = CharBuffer.allocate(65536);
+            CompletableFuture<WebSocket> cfText;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.printf("begin cycle #%s at %s%n",
+                                  i, System.currentTimeMillis());
+                cfText = ws.sendText(data, last);
+                try {
+                    cfText.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                } finally {
+                    System.out.printf("end cycle #%s at %s%n",
+                                      i, System.currentTimeMillis());
+                }
+            }
+            assertFails(ISE, ws.sendText("", true));
+            assertFails(ISE, ws.sendText("", false));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), true));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), false));
+            CompletableFuture<WebSocket> cfPong = ws.sendPong(ByteBuffer.allocate(125));
+            assertHangs(cfPong);
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfClose = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfText);
+            assertFails(IOE, cfPong);
+            assertFails(IOE, cfClose);
+        }
+    }
+
+    @Test(dataProvider = "booleans")
+    public void pendingBinaryPingClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
@@ -211,35 +229,55 @@
                     .join();
 
             ByteBuffer data = ByteBuffer.allocate(65536);
+            CompletableFuture<WebSocket> cfBinary;
             for (int i = 0; ; i++) {  // fill up the send buffer
-                System.out.println("cycle #" + i);
+                System.out.printf("begin cycle #%s at %s%n",
+                                  i, System.currentTimeMillis());
+                cfBinary = ws.sendBinary(data, last);
                 try {
-                    ws.sendBinary(data, true).get(10, TimeUnit.SECONDS);
+                    cfBinary.get(5, TimeUnit.SECONDS);
                     data.clear();
                 } catch (TimeoutException e) {
                     break;
+                } finally {
+                    System.out.printf("end cycle #%s at %s%n",
+                                      i, System.currentTimeMillis());
                 }
             }
+            assertFails(ISE, ws.sendText("", true));
+            assertFails(ISE, ws.sendText("", false));
             assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), true));
-            assertFails(ISE, ws.sendText("", true));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), false));
+            CompletableFuture<WebSocket> cfPing = ws.sendPing(ByteBuffer.allocate(125));
+            assertHangs(cfPing);
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfClose = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfBinary);
+            assertFails(IOE, cfPing);
+            assertFails(IOE, cfClose);
         }
     }
 
-    @Test
-    public void outstanding2() throws Exception {
-        try (DummyWebSocketServer server = notReadingServer()) {
+    @Test(dataProvider = "booleans")
+    public void pendingBinaryPongClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
             server.open();
             WebSocket ws = newHttpClient()
                     .newWebSocketBuilder()
                     .buildAsync(server.getURI(), new WebSocket.Listener() { })
                     .join();
 
-            CharBuffer data = CharBuffer.allocate(65536);
+            ByteBuffer data = ByteBuffer.allocate(65536);
+            CompletableFuture<WebSocket> cfBinary;
             for (int i = 0; ; i++) {  // fill up the send buffer
                 System.out.printf("begin cycle #%s at %s%n",
                                   i, System.currentTimeMillis());
+                cfBinary = ws.sendBinary(data, last);
                 try {
-                    ws.sendText(data, true).get(10, TimeUnit.SECONDS);
+                    cfBinary.get(5, TimeUnit.SECONDS);
                     data.clear();
                 } catch (TimeoutException e) {
                     break;
@@ -249,12 +287,166 @@
                 }
             }
             assertFails(ISE, ws.sendText("", true));
+            assertFails(ISE, ws.sendText("", false));
             assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), true));
+            assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(0), false));
+            CompletableFuture<WebSocket> cfPong = ws.sendPong(ByteBuffer.allocate(125));
+            assertHangs(cfPong);
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfClose = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfBinary);
+            assertFails(IOE, cfPong);
+            assertFails(IOE, cfClose);
+        }
+    }
+
+    @Test(dataProvider = "booleans")
+    public void pendingPingTextClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+
+            ByteBuffer data = ByteBuffer.allocate(125);
+            CompletableFuture<WebSocket> cfPing;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.println("cycle #" + i);
+                cfPing = ws.sendPing(data);
+                try {
+                    cfPing.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                }
+            }
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfText = ws.sendText("hello", last);
+            assertHangs(cfText);
+            CompletableFuture<WebSocket> cfClose
+                    = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfPing);
+            assertFails(IOE, cfText);
+            assertFails(IOE, cfClose);
+        }
+    }
+
+    @Test(dataProvider = "booleans")
+    public void pendingPingBinaryClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+
+            ByteBuffer data = ByteBuffer.allocate(125);
+            CompletableFuture<WebSocket> cfPing;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.println("cycle #" + i);
+                cfPing = ws.sendPing(data);
+                try {
+                    cfPing.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                }
+            }
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfBinary
+                    = ws.sendBinary(ByteBuffer.allocate(4), last);
+            assertHangs(cfBinary);
+            CompletableFuture<WebSocket> cfClose
+                    = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfPing);
+            assertFails(IOE, cfBinary);
+            assertFails(IOE, cfClose);
+        }
+    }
+
+    @Test(dataProvider = "booleans")
+    public void pendingPongTextClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+
+            ByteBuffer data = ByteBuffer.allocate(125);
+            CompletableFuture<WebSocket> cfPong;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.println("cycle #" + i);
+                cfPong = ws.sendPong(data);
+                try {
+                    cfPong.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                }
+            }
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfText = ws.sendText("hello", last);
+            assertHangs(cfText);
+            CompletableFuture<WebSocket> cfClose
+                    = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfPong);
+            assertFails(IOE, cfText);
+            assertFails(IOE, cfClose);
+        }
+    }
+
+    @Test(dataProvider = "booleans")
+    public void pendingPongBinaryClose(boolean last) throws Exception {
+        try (DummyWebSocketServer server = Support.notReadingServer()) {
+            server.open();
+            WebSocket ws = newHttpClient()
+                    .newWebSocketBuilder()
+                    .buildAsync(server.getURI(), new WebSocket.Listener() { })
+                    .join();
+
+            ByteBuffer data = ByteBuffer.allocate(125);
+            CompletableFuture<WebSocket> cfPong;
+            for (int i = 0; ; i++) {  // fill up the send buffer
+                System.out.println("cycle #" + i);
+                cfPong = ws.sendPong(data);
+                try {
+                    cfPong.get(5, TimeUnit.SECONDS);
+                    data.clear();
+                } catch (TimeoutException e) {
+                    break;
+                }
+            }
+            assertFails(ISE, ws.sendPing(ByteBuffer.allocate(125)));
+            assertFails(ISE, ws.sendPong(ByteBuffer.allocate(125)));
+            CompletableFuture<WebSocket> cfBinary
+                    = ws.sendBinary(ByteBuffer.allocate(4), last);
+            assertHangs(cfBinary);
+            CompletableFuture<WebSocket> cfClose
+                    = ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
+            assertHangs(cfClose);
+            ws.abort();
+            assertFails(IOE, cfPong);
+            assertFails(IOE, cfBinary);
+            assertFails(IOE, cfClose);
         }
     }
 
     @Test
-    public void interleavingTypes1() throws IOException {
+    public void partialBinaryThenText() throws IOException {
         try (DummyWebSocketServer server = new DummyWebSocketServer()) {
             server.open();
             WebSocket ws = newHttpClient()
@@ -265,11 +457,14 @@
             ws.sendBinary(ByteBuffer.allocate(16), false).join();
             assertFails(ISE, ws.sendText("text", false));
             assertFails(ISE, ws.sendText("text", true));
+            // Pings & Pongs are fine
+            ws.sendPing(ByteBuffer.allocate(125)).join();
+            ws.sendPong(ByteBuffer.allocate(125)).join();
         }
     }
 
     @Test
-    public void interleavingTypes2() throws IOException {
+    public void partialTextThenBinary() throws IOException {
         try (DummyWebSocketServer server = new DummyWebSocketServer()) {
             server.open();
             WebSocket ws = newHttpClient()
@@ -280,6 +475,9 @@
             ws.sendText("text", false).join();
             assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(16), false));
             assertFails(ISE, ws.sendBinary(ByteBuffer.allocate(16), true));
+            // Pings & Pongs are fine
+            ws.sendPing(ByteBuffer.allocate(125)).join();
+            ws.sendPong(ByteBuffer.allocate(125)).join();
         }
     }
 
@@ -319,7 +517,7 @@
 
     @Test
     public void sendMethodsThrowIOE2() throws Exception {
-        try (DummyWebSocketServer server = serverWithCannedData(0x88, 0x00)) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(0x88, 0x00)) {
             server.open();
             CompletableFuture<Void> onCloseCalled = new CompletableFuture<>();
             CompletableFuture<Void> canClose = new CompletableFuture<>();
@@ -396,7 +594,7 @@
         };
         CompletableFuture<List<byte[]>> actual = new CompletableFuture<>();
 
-        try (DummyWebSocketServer server = serverWithCannedData(binary)) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(binary)) {
             server.open();
 
             WebSocket.Listener listener = new WebSocket.Listener() {
@@ -498,7 +696,7 @@
         };
         CompletableFuture<List<String>> actual = new CompletableFuture<>();
 
-        try (DummyWebSocketServer server = serverWithCannedData(binary)) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(binary)) {
             server.open();
 
             WebSocket.Listener listener = new WebSocket.Listener() {
@@ -588,7 +786,7 @@
         CompletableFuture<List<String>> actual = new CompletableFuture<>();
 
 
-        try (DummyWebSocketServer server = serverWithCannedData(binary)) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(binary)) {
             server.open();
 
             WebSocket.Listener listener = new WebSocket.Listener() {
@@ -668,13 +866,14 @@
     }
 
     /*
-     * The server sends Pong messages. The WebSocket replies to messages automatically.
-     * According to RFC 6455 The WebSocket is free
+     * The server sends Ping messages. The WebSocket replies to these messages
+     * automatically. According to RFC 6455 a WebSocket client is free to reply
+     * only to the most recent Pings.
      */
     @Test(dataProvider = "nPings")
     public void automaticPongs(int nPings) throws Exception {
         // big enough to not bother with resize
-        ByteBuffer buffer = ByteBuffer.allocate(16384);
+        ByteBuffer buffer = ByteBuffer.allocate(65536);
         Frame.HeaderWriter w = new Frame.HeaderWriter();
         for (int i = 0; i < nPings; i++) {
             w.fin(true)
@@ -691,7 +890,7 @@
          .write(buffer);
         buffer.putChar((char) 1000);
         buffer.flip();
-        try (DummyWebSocketServer server = serverWithCannedData(buffer.array())) {
+        try (DummyWebSocketServer server = Support.serverWithCannedData(buffer.array())) {
             MockListener listener = new MockListener();
             server.open();
             WebSocket ws = newHttpClient()
@@ -768,4 +967,9 @@
     public Object[][] nPings() {
         return new Object[][]{{1}, {2}, {4}, {8}, {9}, {1023}};
     }
+
+    @DataProvider(name = "booleans")
+    public Object[][] booleans() {
+        return new Object[][]{{Boolean.TRUE}, {Boolean.FALSE}};
+    }
 }