Repository: netty/netty
Issue: Websocket client closing handshake to support "force close" after given timetout
Labels: defect
improvement
feature

RFC455 defines that, generally, a WebSocket client should not close a TCP connection as far as a server is the one who's responsible for doing that. In practice, when I do not control the server, this might lead to connections leak if the server does not comply with the RFC.

[RFC455#7.1.1](https://tools.ietf.org/html/rfc6455#section-7.1.1) says

> In abnormal cases (such as not having received a TCP Close from the server after a reasonable amount of time) a client MAY initiate the TCP Close.

I suggest to add `forceCloseAfterMillis` option to `WebSocketClientHandshaker` (and to `WebSocketClientProtocolHandler` appropriately) and schedule a channel close when necessary after `CloseWebSocketFrame` being flushed [here](https://github.com/netty/netty/blob/f176384a729c1d9352c9ed878b9b967ca2f31bf8/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java#L430-L435). If there's no objections, I'm happy to provide a PR.

Issue comments:
@kachayev this sounds good... Its also inline with what we do in SslHandler. 
@normanmaurer I'll do a PR for WebSockets first, as I know this code quite well. Will check `SslHandler` later ;) 
Sorry maybe I was not clear here... we have this kind of code already in SslHandler. I just wanted to mention it as it may gives you some good examples 
Ah, okay! Now it's clear. I thought you meant that we need to do the same for SSL. Thanks! 

Pull request review comments:
why is this volatile ?
private final ?
remove public
why do you retain here ? This looks like a possible memory leak.
assert both return values
Previously, I had an API to change this value after a handshaker is already created. As far as I removed that, I can remove `volatile` either.
Yeah, that's a bug
nit: you may also want to check `!forceCloseInit.get()`
Use a timeout as we may never complete the loop here when there is a bug:

https://github.com/netty/netty/pull/8896/files#diff-96a945171dadb844c094502b4d37149bR164
nice!
either remove the method or fix the styling to use:

```java
private void setForceCloseComplete() {
    ....
}
```

I would personally just remove it.
Removed.
I think you should override this one in each implementation to return the correct type. 
Done!
nit: jut call `super.setForceCloseTimeoutMillis(...)` this will also ensure we do the correct thing if we ever change the super method content and will allow to make `forceCloseTimeMillis` private
nit: jut call `super.setForceCloseTimeoutMillis(...)` this will also ensure we do the correct thing if we ever change the super method content and will allow to make `forceCloseTimeMillis` private
nit: jut call `super.setForceCloseTimeoutMillis(...)` this will also ensure we do the correct thing if we ever change the super method content and will allow to make `forceCloseTimeMillis` private
nit: jut call `super.setForceCloseTimeoutMillis(...)` this will also ensure we do the correct thing if we ever change the super method content and will allow to make `forceCloseTimeMillis` private
nit: you can remove the java docs as it just overrides a method.
nit: you can remove the java docs as it just overrides a method.
nit: you can remove the java docs as it just overrides a method.
nit: you can remove the java docs as it just overrides a method.
as this is protected we will need to also retain the old constructor as otherwise its API breakage:

```
20:53:51 [ERROR] Failed to execute goal com.github.siom79.japicmp:japicmp-maven-plugin:0.13.1:cmp (default) on project netty-codec-http: There is at least one incompatibility: io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker.WebSocketClientHandshaker(java.net.URI,io.netty.handler.codec.http.websocketx.WebSocketVersion,java.lang.String,io.netty.handler.codec.http.HttpHeaders,int):CONSTRUCTOR_REMOVED -> [Help 1]
```
Done!
nit: add a whitespace after `,`
Done!
Could you use `Atomic*FieldUpdater` instead?
Could extract the default value into a static final field.
Could you add the Javadoc summary, because otherwise the generated Javadoc will have empty summary.
How about:

```
Creates a new instance with the specified destination WebSocket location and version to initiate.
```
nit: do you really need `ScheduledFuture` instead of `Future` as you don't use any method from `Delayed`?
nit: more of a coding style preference, but I'd rather have one single boolean operation wrapping the method's body: 
`if (future.isSuccess() && channel.isActive() && forceCloseInit.compareAndSet(false, true))`

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
index 44c91445a2a..b293614c16d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
@@ -40,6 +40,9 @@
 import java.net.URI;
 import java.nio.channels.ClosedChannelException;
 import java.util.Locale;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
 /**
  * Base class for web socket client handshake implementations
@@ -50,6 +53,7 @@ public abstract class WebSocketClientHandshaker {
 
     private static final String HTTP_SCHEME_PREFIX = HttpScheme.HTTP + "://";
     private static final String HTTPS_SCHEME_PREFIX = HttpScheme.HTTPS + "://";
+    protected static final int DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS = 10000;
 
     private final URI uri;
 
@@ -57,6 +61,15 @@ public abstract class WebSocketClientHandshaker {
 
     private volatile boolean handshakeComplete;
 
+    private volatile long forceCloseTimeoutMillis = DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS;
+
+    private volatile int forceCloseInit;
+
+    private static final AtomicIntegerFieldUpdater<WebSocketClientHandshaker> FORCE_CLOSE_INIT_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(WebSocketClientHandshaker.class, "forceCloseInit");
+
+    private volatile boolean forceCloseComplete;
+
     private final String expectedSubprotocol;
 
     private volatile String actualSubprotocol;
@@ -82,11 +95,35 @@ public abstract class WebSocketClientHandshaker {
      */
     protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
                                         HttpHeaders customHeaders, int maxFramePayloadLength) {
+        this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param uri
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
+                                        HttpHeaders customHeaders, int maxFramePayloadLength,
+                                        long forceCloseTimeoutMillis) {
         this.uri = uri;
         this.version = version;
         expectedSubprotocol = subprotocol;
         this.customHeaders = customHeaders;
         this.maxFramePayloadLength = maxFramePayloadLength;
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
     }
 
     /**
@@ -140,6 +177,29 @@ private void setActualSubprotocol(String actualSubprotocol) {
         this.actualSubprotocol = actualSubprotocol;
     }
 
+    public long forceCloseTimeoutMillis() {
+        return forceCloseTimeoutMillis;
+    }
+
+    /**
+     * Flag to indicate if the closing handshake was initiated because of timeout.
+     * For testing only.
+     */
+    protected boolean isForceCloseComplete() {
+        return forceCloseComplete;
+    }
+
+    /**
+     * Sets timeout to close the connection if it was not closed by the server.
+     *
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public WebSocketClientHandshaker setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+        return this;
+    }
+
     /**
      * Begins the opening handshake
      *
@@ -431,7 +491,46 @@ public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPr
         if (channel == null) {
             throw new NullPointerException("channel");
         }
-        return channel.writeAndFlush(frame, promise);
+        channel.writeAndFlush(frame, promise);
+        applyForceCloseTimeout(channel, promise);
+        return promise;
+    }
+
+    private void applyForceCloseTimeout(final Channel channel, ChannelFuture flushFuture) {
+        final long forceCloseTimeoutMillis = this.forceCloseTimeoutMillis;
+        final WebSocketClientHandshaker handshaker = this;
+        if (forceCloseTimeoutMillis <= 0 || !channel.isActive() || forceCloseInit != 0) {
+            return;
+        }
+
+        flushFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                // If flush operation failed, there is no reason to expect
+                // a server to receive CloseFrame. Thus this should be handled
+                // by the application separately.
+                // Also, close might be called twice from different threads.
+                if (future.isSuccess() && channel.isActive() &&
+                        FORCE_CLOSE_INIT_UPDATER.compareAndSet(handshaker, 0, 1)) {
+                    final Future<?> forceCloseFuture = channel.eventLoop().schedule(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (channel.isActive()) {
+                                channel.close();
+                                forceCloseComplete = true;
+                            }
+                        }
+                    }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS);
+
+                    channel.closeFuture().addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) throws Exception {
+                            forceCloseFuture.cancel(false);
+                        }
+                    });
+                }
+            }
+        });
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
index f02626319f3..3b43bd3ae58 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
@@ -48,7 +48,7 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
     private ByteBuf expectedChallengeResponseBytes;
 
     /**
-     * Constructor specifying the destination web socket location and version to initiate
+     * Creates a new instance with the specified destination WebSocket location and version to initiate.
      *
      * @param webSocketURL
      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
@@ -64,7 +64,31 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
             HttpHeaders customHeaders, int maxFramePayloadLength) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+        this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength,
+                DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance with the specified destination WebSocket location and version to initiate.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       long forceCloseTimeoutMillis) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
     }
 
     /**
@@ -243,4 +267,11 @@ protected WebSocketFrameDecoder newWebsocketDecoder() {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket00FrameEncoder();
     }
+
+    @Override
+    public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
index 4632a4aecb6..c10132989f1 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
@@ -94,10 +94,43 @@ public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, S
      *            When set to true, frames which are not masked properly according to the standard will still be
      *            accepted.
      */
+    public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     */
     public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
-            boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -216,4 +249,11 @@ protected WebSocketFrameDecoder newWebsocketDecoder() {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket07FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker07 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
index 1a11aa6358e..237d2f715ed 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
@@ -68,7 +68,8 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
-        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true,
+                false, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
     }
 
     /**
@@ -93,12 +94,45 @@ public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, S
      *            which doesn't require masking might set this to false to achieve a higher performance.
      * @param allowMaskMismatch
      *            When set to true, frames which are not masked properly according to the standard will still be
-     *            accepted.
+     *            accepted
+     */
+    public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
      */
     public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
-            boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -217,4 +251,11 @@ protected WebSocketFrameDecoder newWebsocketDecoder() {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket08FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker08 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
index 808f7fc49ad..a96683f2d11 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
@@ -68,7 +68,8 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
-        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+                true, false);
     }
 
     /**
@@ -98,7 +99,41 @@ public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, S
     public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
             boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+                performMasking, allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     */
+    public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch,
+                                       long forceCloseTimeoutMillis) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -217,4 +252,11 @@ protected WebSocketFrameDecoder newWebsocketDecoder() {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket13FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker13 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
index b07825f4a17..22afc3bd764 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
@@ -107,24 +107,59 @@ public static WebSocketClientHandshaker newHandshaker(
             URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
             boolean performMasking, boolean allowMaskMismatch) {
+        return newHandshaker(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
+                maxFramePayloadLength, true, false, -1);
+    }
+
+    /**
+     * Creates a new handshaker.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath".
+     *            Subsequent web socket frames will be sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server. Null if no sub-protocol support is required.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Custom HTTP headers to send during the handshake
+     * @param maxFramePayloadLength
+     *            Maximum allowable frame payload length. Setting this value to your application's
+     *            requirement may reduce denial of service attacks using long data frames.
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted.
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public static WebSocketClientHandshaker newHandshaker(
+            URI webSocketURL, WebSocketVersion version, String subprotocol,
+            boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
         if (version == V13) {
             return new WebSocketClientHandshaker13(
                     webSocketURL, V13, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
         }
         if (version == V08) {
             return new WebSocketClientHandshaker08(
                     webSocketURL, V08, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
         }
         if (version == V07) {
             return new WebSocketClientHandshaker07(
                     webSocketURL, V07, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
         }
         if (version == V00) {
             return new WebSocketClientHandshaker00(
-                    webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength);
+                    webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
         }
 
         throw new WebSocketHandshakeException("Protocol version " + version + " not supported.");


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
index 2054af513f9..5cb1e0e7b62 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
@@ -217,7 +217,7 @@ private void testHttpResponseAndFrameInSameBuffer(boolean codec) {
         String url = "ws://localhost:9999/ws";
         final WebSocketClientHandshaker shaker = newHandshaker(URI.create(url));
         final WebSocketClientHandshaker handshaker = new WebSocketClientHandshaker(
-                shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE) {
+                shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, -1) {
             @Override
             protected FullHttpRequest newHandshakeRequest() {
                 return shaker.newHandshakeRequest();
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
index 10ad7749845..5583a27339e 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
@@ -17,10 +17,13 @@
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.EmptyHttpHeaders;
 import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpServerCodec;
@@ -30,6 +33,7 @@
 import org.junit.Test;
 
 import java.net.URI;
+import java.util.List;
 
 import static org.junit.Assert.*;
 
@@ -39,6 +43,23 @@ public class WebSocketHandshakeHandOverTest {
     private WebSocketServerProtocolHandler.HandshakeComplete serverHandshakeComplete;
     private boolean clientReceivedHandshake;
     private boolean clientReceivedMessage;
+    private boolean serverReceivedCloseHandshake;
+    private boolean clientForceClosed;
+
+    private final class CloseNoOpServerProtocolHandler extends WebSocketServerProtocolHandler {
+        CloseNoOpServerProtocolHandler(String websocketPath) {
+            super(websocketPath, null, false);
+        }
+
+        @Override
+        protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
+            if (frame instanceof CloseWebSocketFrame) {
+                serverReceivedCloseHandshake = true;
+                return;
+            }
+            super.decode(ctx, frame, out);
+        }
+    }
 
     @Before
     public void setUp() {
@@ -46,6 +67,8 @@ public void setUp() {
         serverHandshakeComplete = null;
         clientReceivedHandshake = false;
         clientReceivedMessage = false;
+        serverReceivedCloseHandshake = false;
+        clientForceClosed = false;
     }
 
     @Test
@@ -95,6 +118,64 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Except
         assertTrue(clientReceivedMessage);
     }
 
+    @Test(timeout = 10000)
+    public void testClientHandshakerForceClose() throws Exception {
+        final WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+                new URI("ws://localhost:1234/test"), WebSocketVersion.V13, null, true,
+                EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, true, false, 20);
+
+        EmbeddedChannel serverChannel = createServerChannel(
+                new CloseNoOpServerProtocolHandler("/test"),
+                new SimpleChannelInboundHandler<Object>() {
+                    @Override
+                    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+                    }
+                });
+
+        EmbeddedChannel clientChannel = createClientChannel(handshaker, new SimpleChannelInboundHandler<Object>() {
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                if (evt == ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
+                    ctx.channel().closeFuture().addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) throws Exception {
+                            clientForceClosed = true;
+                        }
+                    });
+                    handshaker.close(ctx.channel(), new CloseWebSocketFrame());
+                }
+            }
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+            }
+        });
+
+        // Transfer the handshake from the client to the server
+        transferAllDataWithMerge(clientChannel, serverChannel);
+        // Transfer the handshake from the server to client
+        transferAllDataWithMerge(serverChannel, clientChannel);
+
+        // Transfer closing handshake
+        transferAllDataWithMerge(clientChannel, serverChannel);
+        assertTrue(serverReceivedCloseHandshake);
+        // Should not be closed yet as we disabled closing the connection on the server
+        assertFalse(clientForceClosed);
+
+        while (!clientForceClosed) {
+            Thread.sleep(10);
+            // We need to run all pending tasks as the force close timeout is scheduled on the EventLoop.
+            clientChannel.runPendingTasks();
+        }
+
+        // clientForceClosed would be set to TRUE after any close,
+        // so check here that force close timeout was actually fired
+        assertTrue(handshaker.isForceCloseComplete());
+
+        // Both should be empty
+        assertFalse(serverChannel.finishAndReleaseAll());
+        assertFalse(clientChannel.finishAndReleaseAll());
+    }
+
     /**
      * Transfers all pending data from the source channel into the destination channel.<br>
      * Merges all data into a single buffer before transmission into the destination.
@@ -137,6 +218,16 @@ private static EmbeddedChannel createClientChannel(ChannelHandler handler) throw
                 handler);
     }
 
+    private static EmbeddedChannel createClientChannel(WebSocketClientHandshaker handshaker,
+                                                       ChannelHandler handler) throws Exception {
+        return new EmbeddedChannel(
+                new HttpClientCodec(),
+                new HttpObjectAggregator(8192),
+                // Note that we're switching off close frames handling on purpose to test forced close on timeout.
+                new WebSocketClientProtocolHandler(handshaker, false, false),
+                handler);
+    }
+
     private static EmbeddedChannel createServerChannel(ChannelHandler handler) {
         return new EmbeddedChannel(
                 new HttpServerCodec(),
@@ -144,4 +235,14 @@ private static EmbeddedChannel createServerChannel(ChannelHandler handler) {
                 new WebSocketServerProtocolHandler("/test", "test-proto-1, test-proto-2", false),
                 handler);
     }
+
+    private static EmbeddedChannel createServerChannel(WebSocketServerProtocolHandler webSocketHandler,
+                                                       ChannelHandler handler) {
+        return new EmbeddedChannel(
+                new HttpServerCodec(),
+                new HttpObjectAggregator(8192),
+                webSocketHandler,
+                handler);
+    }
+
 }