Add support for the websocket close status code and reason fields.

Closes #890.
This commit is contained in:
Ben Darnell 2014-02-17 18:46:40 -05:00
parent 175da1055a
commit 58d5ffc26b
2 changed files with 80 additions and 10 deletions

View File

@ -36,7 +36,7 @@ class TestWebSocketHandler(WebSocketHandler):
self.close_future = close_future self.close_future = close_future
def on_close(self): def on_close(self):
self.close_future.set_result(None) self.close_future.set_result((self.close_code, self.close_reason))
class EchoHandler(TestWebSocketHandler): class EchoHandler(TestWebSocketHandler):
@ -54,6 +54,11 @@ class NonWebSocketHandler(RequestHandler):
self.write('ok') self.write('ok')
class CloseReasonHandler(TestWebSocketHandler):
def open(self):
self.close(1001, "goodbye")
class WebSocketTest(AsyncHTTPTestCase): class WebSocketTest(AsyncHTTPTestCase):
def get_app(self): def get_app(self):
self.close_future = Future() self.close_future = Future()
@ -61,6 +66,8 @@ class WebSocketTest(AsyncHTTPTestCase):
('/echo', EchoHandler, dict(close_future=self.close_future)), ('/echo', EchoHandler, dict(close_future=self.close_future)),
('/non_ws', NonWebSocketHandler), ('/non_ws', NonWebSocketHandler),
('/header', HeaderHandler, dict(close_future=self.close_future)), ('/header', HeaderHandler, dict(close_future=self.close_future)),
('/close_reason', CloseReasonHandler,
dict(close_future=self.close_future)),
]) ])
@gen_test @gen_test
@ -147,6 +154,25 @@ class WebSocketTest(AsyncHTTPTestCase):
ws.close() ws.close()
yield self.close_future yield self.close_future
@gen_test
def test_server_close_reason(self):
ws = yield websocket_connect(
'ws://localhost:%d/close_reason' % self.get_http_port())
msg = yield ws.read_message()
# A message of None means the other side closed the connection.
self.assertIs(msg, None)
self.assertEqual(ws.close_code, 1001)
self.assertEqual(ws.close_reason, "goodbye")
@gen_test
def test_client_close_reason(self):
ws = yield websocket_connect(
'ws://localhost:%d/echo' % self.get_http_port())
ws.close(1001, 'goodbye')
code, reason = yield self.close_future
self.assertEqual(code, 1001)
self.assertEqual(reason, 'goodbye')
class MaskFunctionMixin(object): class MaskFunctionMixin(object):
# Subclasses should define self.mask(mask, data) # Subclasses should define self.mask(mask, data)

View File

@ -32,7 +32,7 @@ import tornado.escape
import tornado.web import tornado.web
from tornado.concurrent import TracebackFuture from tornado.concurrent import TracebackFuture
from tornado.escape import utf8, native_str from tornado.escape import utf8, native_str, to_unicode
from tornado import httpclient, httputil from tornado import httpclient, httputil
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.iostream import StreamClosedError from tornado.iostream import StreamClosedError
@ -110,6 +110,8 @@ class WebSocketHandler(tornado.web.RequestHandler):
**kwargs) **kwargs)
self.stream = request.connection.stream self.stream = request.connection.stream
self.ws_connection = None self.ws_connection = None
self.close_code = None
self.close_reason = None
def _execute(self, transforms, *args, **kwargs): def _execute(self, transforms, *args, **kwargs):
self.open_args = args self.open_args = args
@ -220,16 +222,39 @@ class WebSocketHandler(tornado.web.RequestHandler):
pass pass
def on_close(self): def on_close(self):
"""Invoked when the WebSocket is closed.""" """Invoked when the WebSocket is closed.
If the connection was closed cleanly and a status code or reason
phrase was supplied, these values will be available as the attributes
``self.close_code`` and ``self.close_reason``.
.. versionchanged:: 3.3
Added ``close_code`` and ``close_reason`` attributes.
"""
pass pass
def close(self): def close(self, code=None, reason=None):
"""Closes this Web Socket. """Closes this Web Socket.
Once the close handshake is successful the socket will be closed. Once the close handshake is successful the socket will be closed.
``code`` may be a numeric status code, taken from the values
defined in `RFC 6455 section 7.4.1
<https://tools.ietf.org/html/rfc6455#section-7.4.1>`_.
``reason`` may be a textual message about why the connection is
closing. These values are made available to the client, but are
not otherwise interpreted by the websocket protocol.
The ``code`` and ``reason`` arguments are ignored in the "draft76"
protocol version.
.. versionchanged:: 3.3
Added the ``code`` and ``reason`` arguments.
""" """
if self.ws_connection: if self.ws_connection:
self.ws_connection.close() self.ws_connection.close(code, reason)
self.ws_connection = None self.ws_connection = None
def allow_draft76(self): def allow_draft76(self):
@ -489,7 +514,7 @@ class WebSocketProtocol76(WebSocketProtocol):
"""Send ping frame.""" """Send ping frame."""
raise ValueError("Ping messages not supported by this version of websockets") raise ValueError("Ping messages not supported by this version of websockets")
def close(self): def close(self, code=None, reason=None):
"""Closes the WebSocket connection.""" """Closes the WebSocket connection."""
if not self.server_terminated: if not self.server_terminated:
if not self.stream.closed(): if not self.stream.closed():
@ -739,6 +764,10 @@ class WebSocketProtocol13(WebSocketProtocol):
elif opcode == 0x8: elif opcode == 0x8:
# Close # Close
self.client_terminated = True self.client_terminated = True
if len(data) >= 2:
self.handler.close_code = struct.unpack('>H', data[:2])[0]
if len(data) > 2:
self.handler.close_reason = to_unicode(data[2:])
self.close() self.close()
elif opcode == 0x9: elif opcode == 0x9:
# Ping # Ping
@ -749,11 +778,19 @@ class WebSocketProtocol13(WebSocketProtocol):
else: else:
self._abort() self._abort()
def close(self): def close(self, code=None, reason=None):
"""Closes the WebSocket connection.""" """Closes the WebSocket connection."""
if not self.server_terminated: if not self.server_terminated:
if not self.stream.closed(): if not self.stream.closed():
self._write_frame(True, 0x8, b"") if code is None and reason is not None:
code = 1000 # "normal closure" status code
if code is None:
close_data = b''
else:
close_data = struct.pack('>H', code)
if reason is not None:
close_data += utf8(reason)
self._write_frame(True, 0x8, close_data)
self.server_terminated = True self.server_terminated = True
if self.client_terminated: if self.client_terminated:
if self._waiting is not None: if self._waiting is not None:
@ -794,13 +831,20 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
io_loop, None, request, lambda: None, self._on_http_response, io_loop, None, request, lambda: None, self._on_http_response,
104857600, self.resolver) 104857600, self.resolver)
def close(self): def close(self, code=None, reason=None):
"""Closes the websocket connection. """Closes the websocket connection.
``code`` and ``reason`` are documented under
`WebSocketHandler.close`.
.. versionadded:: 3.2 .. versionadded:: 3.2
.. versionchanged:: 3.3
Added the ``code`` and ``reason`` arguments.
""" """
if self.protocol is not None: if self.protocol is not None:
self.protocol.close() self.protocol.close(code, reason)
self.protocol = None self.protocol = None
def _on_close(self): def _on_close(self):