Repository: netty/netty
Issue: [rfe] [websocket] Close cause code should be available

When closing a WebSocket connection using Javascript, it is possible to learn the closing 'cause'. As an example

```
                _websocket.onclose = function(message) {
                    var reason = message.reason;
                    if (reason === "") {
                        switch (message.code) {
                            case 1000:
                                reason = "Normal closure; the connection successfully completed whatever purpose for which " +
                                    "it was created.";
                                break;
                            case 1001:
                                reason = "The endpoint is going away, either because of a server failure or because the " +
                                    "browser is navigating away from the page that opened the connection.";
                                break;
                            case 1002:
                                reason = "The endpoint is terminating the connection due to a protocol error.";
                                break;
                            case 1003:
                                reason = "The connection is being terminated because the endpoint received data of a type it " +
                                    "cannot accept (for example, a text-only endpoint received binary data).";
                                break;
                            case 1004:
                                reason = "The endpoint is terminating the connection because a data frame was received that " +
                                    "is too large.";
                                break;
                            case 1005:
                                reason = "Unknown: no status code was provided even though one was expected.";
                                break;
                            case 1006:
                                reason = "Connection was closed abnormally (that is, with no close frame being sent).";
                                break;
                        }
                    }
```

It is possible from Javascript to customize the error message and the associated logic. The Jetty Server is able to detect the close cause so it should be possible, using NIO, to support it.


Issue comments:
Hi Jeanfrancois,

I just need clarification of your use case.  Is your request for Netty to:
1. Extract error status/messages sent from the browser
2. Send error status/message to the browser
3. Both?

Just want to make sure I fully understand your use case.

Thanks.

Hi,

it's mostly for [AHC](https://github.com/sonatype/async-http-client). Right now when the server close the connection I do get called back with the close event, but I don't know the state of the 'close'. Was it closed by the client, server, a network outage etc. This is the information I would like to have. 

Now server side it is the same "problem", for [Nettosphere](https://github.com/Atmosphere/nettosphere) I also want to know the close reason. 

I was using Jetty as an example because Atmosphere supports it and get called back with the proper 'close' code. That's why I think it is doable in Netty/NIO as well.

Does it help?

Thanks

-- Jeanfrancois

Thanks, yes it does help my understanding.

Closing status and message is an optional part of the spec (http://tools.ietf.org/html/rfc6455#section-5.5.1) that I have yet to implement.

I will add extra properties and constructors to CloseWebSocketFrame to support the encoding/decoding of status and message.

I'll try to get some time over the Easter break to submit the pull request.

Thanks

Hi Jeanfrancois,

I've just merged the code for this in master and version 3 branch.  I've run the code against AutoBahn is it seems to be working ok.  Please help test it and let us know if it is working correctly for you.

To send the status code on the client to the server, you will just have to supply the code and reason on the constructor:

```
new CloseWebSocketFrame(1000, "Some text message")
```

To read the status code and reason on the server, you will just have to:

```
closeFrame.getStatusCode()
closeFrame.getReasonText()
```

Regards
Vibul

Thanks. I will take a look today and report as soon as I can.

Thanks Norman for fixing that.

Salut, this works like a charm. AHC is now using 3.4.1-Final and the feature will be added in the upcoming 1.7.4 release. Thanks again!


Pull request review comments:

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
index 1f8ed460282..8ed9acb20b5 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
@@ -15,7 +15,9 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.buffer.ChannelBuffer;
 import io.netty.buffer.ChannelBuffers;
+import io.netty.util.CharsetUtil;
 
 /**
  * Web Socket Frame for closing the connection
@@ -30,7 +32,20 @@ public CloseWebSocketFrame() {
     }
 
     /**
-     * Creates a new close frame
+     * Creates a new empty close frame with closing status code and reason text
+     * 
+     * @param statusCode
+     *            Integer status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For
+     *            example, <tt>1000</tt> indicates normal closure.
+     * @param reasonText
+     *            Reason text. Set to null if no text.
+     */
+    public CloseWebSocketFrame(int statusCode, String reasonText) {
+        this(true, 0, statusCode, reasonText);
+    }
+
+    /**
+     * Creates a new close frame with no losing status code and no reason text
      * 
      * @param finalFragment
      *            flag indicating if this frame is the final fragment
@@ -38,8 +53,93 @@ public CloseWebSocketFrame() {
      *            reserved bits used for protocol extensions
      */
     public CloseWebSocketFrame(boolean finalFragment, int rsv) {
+        this(finalFragment, rsv, null);
+    }
+
+    /**
+     * Creates a new close frame with closing status code and reason text
+     * 
+     * @param finalFragment
+     *            flag indicating if this frame is the final fragment
+     * @param rsv
+     *            reserved bits used for protocol extensions
+     * @param statusCode
+     *            Integer status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For
+     *            example, <tt>1000</tt> indicates normal closure.
+     * @param reasonText
+     *            Reason text. Set to null if no text.
+     */
+    public CloseWebSocketFrame(boolean finalFragment, int rsv, int statusCode, String reasonText) {
+        setFinalFragment(finalFragment);
+        setRsv(rsv);
+
+        byte[] reasonBytes = new byte[0];
+        if (reasonText != null) {
+            reasonBytes = reasonText.getBytes(CharsetUtil.UTF_8);
+        }
+
+        ChannelBuffer binaryData = ChannelBuffers.buffer(2 + reasonBytes.length);
+        binaryData.writeShort(statusCode);
+        if (reasonBytes.length > 0) {
+            binaryData.writeBytes(reasonBytes);
+        }
+
+        binaryData.readerIndex(0);
+        setBinaryData(binaryData);
+    }
+
+    /**
+     * Creates a new close frame
+     * 
+     * @param finalFragment
+     *            flag indicating if this frame is the final fragment
+     * @param rsv
+     *            reserved bits used for protocol extensions
+     * @param binaryData
+     *            the content of the frame. Must be 2 byte integer followed by optional UTF-8 encoded string.
+     */
+    public CloseWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) {
         setFinalFragment(finalFragment);
         setRsv(rsv);
+        if (binaryData == null) {
+            setBinaryData(ChannelBuffers.EMPTY_BUFFER);
+        } else {
+            setBinaryData(binaryData);
+        }
+    }
+
+    /**
+     * Returns the closing status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. If
+     * a status code is set, -1 is returned.
+     */
+    public int getStatusCode() {
+        ChannelBuffer binaryData = this.getBinaryData();
+        if (binaryData == null || binaryData.capacity() == 0) {
+            return -1;
+        }
+
+        binaryData.readerIndex(0);
+        int statusCode = binaryData.readShort();
+        binaryData.readerIndex(0);
+
+        return statusCode;
+    }
+
+    /**
+     * Returns the reason text as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a> If a reason
+     * text is not supplied, an empty string is returned.
+     */
+    public String getReasonText() {
+        ChannelBuffer binaryData = this.getBinaryData();
+        if (binaryData == null || binaryData.capacity() <= 2) {
+            return "";
+        }
+
+        binaryData.readerIndex(2);
+        String reasonText = binaryData.toString(CharsetUtil.UTF_8);
+        binaryData.readerIndex(0);
+
+        return reasonText;
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
index 5bd7ea00387..b590c11d8a9 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
@@ -284,8 +284,9 @@ protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffe
             } else if (frameOpcode == OPCODE_PONG) {
                 return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
             } else if (frameOpcode == OPCODE_CLOSE) {
+                checkCloseFrameBody(channel, framePayload);
                 receivedClosingHandshake = true;
-                return new CloseWebSocketFrame(frameFinalFlag, frameRsv);
+                return new CloseWebSocketFrame(frameFinalFlag, frameRsv, framePayload);
             }
 
             // Processing for possible fragmented messages for text and binary
@@ -391,4 +392,38 @@ private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFram
             protocolViolation(channel, "invalid UTF-8 bytes");
         }
     }
+
+    protected void checkCloseFrameBody(Channel channel, ChannelBuffer buffer) throws CorruptedFrameException {
+        if (buffer == null || buffer.capacity() == 0) {
+            return;
+        }
+        if (buffer.capacity() == 1) {
+            protocolViolation(channel, "Invalid close frame body");
+        }
+
+        // Save reader index
+        int idx = buffer.readerIndex();
+        buffer.readerIndex(0);
+
+        // Must have 2 byte integer within the valid range
+        int statusCode = buffer.readShort();
+        if ((statusCode >= 0 && statusCode <= 999) || (statusCode >= 1004 && statusCode <= 1006)
+                || (statusCode >= 1012 && statusCode <= 2999)) {
+            protocolViolation(channel, "Invalid close frame status code: " + statusCode);
+        }
+
+        // May have UTF-8 message
+        if (buffer.readableBytes() > 0) {
+            byte[] b = new byte[buffer.readableBytes()];
+            buffer.readBytes(b);
+            try {
+                new UTF8Output(b);
+            } catch (UTF8Exception ex) {
+                protocolViolation(channel, "Invalid close frame reason text. Invalid UTF-8 bytes");
+            }
+        }
+        
+        // Restore reader index
+        buffer.readerIndex(idx);
+    }
 }
diff --git a/example/src/main/java/io/netty/example/http/websocketx/autobahn/AutobahnServerHandler.java b/example/src/main/java/io/netty/example/http/websocketx/autobahn/AutobahnServerHandler.java
index 0729d0b5ad6..d04cb842c42 100644
--- a/example/src/main/java/io/netty/example/http/websocketx/autobahn/AutobahnServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/websocketx/autobahn/AutobahnServerHandler.java
@@ -82,11 +82,10 @@ private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throw
 
     private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
         if (logger.isDebugEnabled()) {
-            logger.debug(String
-                    .format("Channel %s received %s", ctx.getChannel().getId(), frame.getClass().getSimpleName()));
+            logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), frame.getClass()
+                    .getSimpleName()));
         }
 
-
         if (frame instanceof CloseWebSocketFrame) {
             this.handshaker.close(ctx.getChannel(), (CloseWebSocketFrame) frame);
         } else if (frame instanceof PingWebSocketFrame) {


Test patch: