Skip to content

Commit

Permalink
Provide an enum for close codes.
Browse files Browse the repository at this point in the history
Fix #1335.
  • Loading branch information
aaugustin committed May 18, 2023
1 parent 89fc408 commit 2b627b2
Show file tree
Hide file tree
Showing 27 changed files with 480 additions and 269 deletions.
8 changes: 4 additions & 4 deletions docs/faq/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ There are several reasons why long-lived connections may be lost:
If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to
see when and how connections are closed.

What does ``ConnectionClosedError: sent 1011 (unexpected error) keepalive ping timeout; no close frame received`` mean?
-----------------------------------------------------------------------------------------------------------------------
What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean?
---------------------------------------------------------------------------------------------------------------------

If you're seeing this traceback in the logs of a server:

Expand All @@ -70,7 +70,7 @@ If you're seeing this traceback in the logs of a server:
Traceback (most recent call last):
...
websockets.exceptions.ConnectionClosedError: sent 1011 (unexpected error) keepalive ping timeout; no close frame received
websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
or if a client crashes with this traceback:

Expand All @@ -84,7 +84,7 @@ or if a client crashes with this traceback:
Traceback (most recent call last):
...
websockets.exceptions.ConnectionClosedError: sent 1011 (unexpected error) keepalive ping timeout; no close frame received
websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
it means that the WebSocket connection suffered from excessive latency and was
closed after reaching the timeout of websockets' keepalive mechanism.
Expand Down
2 changes: 1 addition & 1 deletion docs/howto/django.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ closes the connection:
$ python -m websockets ws://localhost:8888/
Connected to ws://localhost:8888.
> not a token
Connection closed: 1011 (unexpected error) authentication failed.
Connection closed: 1011 (internal error) authentication failed.
You can also test from a browser by generating a new token and running the
following code in the JavaScript console of the browser:
Expand Down
3 changes: 1 addition & 2 deletions docs/howto/fly.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,4 @@ away).
Connection closed: 1001 (going away).
If graceful shutdown wasn't working, the server wouldn't perform a closing
handshake and the connection would be closed with code 1006 (connection closed
abnormally).
handshake and the connection would be closed with code 1006 (abnormal closure).
3 changes: 1 addition & 2 deletions docs/howto/heroku.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,4 @@ away).
Connection closed: 1001 (going away).
If graceful shutdown wasn't working, the server wouldn't perform a closing
handshake and the connection would be closed with code 1006 (connection closed
abnormally).
handshake and the connection would be closed with code 1006 (abnormal closure).
2 changes: 1 addition & 1 deletion docs/howto/kubernetes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ shut down gracefully:
< Hey there!
Connection closed: 1001 (going away).
If it didn't, you'd get code 1006 (connection closed abnormally).
If it didn't, you'd get code 1006 (abnormal closure).

Deploy application
------------------
Expand Down
3 changes: 1 addition & 2 deletions docs/howto/render.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ deployment completes, the connection is closed with code 1001 (going away).
Connection closed: 1001 (going away).
If graceful shutdown wasn't working, the server wouldn't perform a closing
handshake and the connection would be closed with code 1006 (connection closed
abnormally).
handshake and the connection would be closed with code 1006 (abnormal closure).

Remember to downgrade to a free plan if you upgraded just for testing this feature.
5 changes: 5 additions & 0 deletions docs/project/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ Backwards-incompatible changes

websockets 11.0 is the last version supporting Python 3.7.

Improvements
............

* Added :class:`~frames.CloseCode`.

11.0.3
------

Expand Down
23 changes: 18 additions & 5 deletions docs/reference/datastructures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,32 @@ WebSocket events
.. autoclass:: Opcode

.. autoattribute:: CONT

.. autoattribute:: TEXT

.. autoattribute:: BINARY

.. autoattribute:: CLOSE

.. autoattribute:: PING

.. autoattribute:: PONG

.. autoclass:: Close

.. autoclass:: CloseCode

.. autoattribute:: OK
.. autoattribute:: GOING_AWAY
.. autoattribute:: PROTOCOL_ERROR
.. autoattribute:: UNSUPPORTED_DATA
.. autoattribute:: NO_STATUS_RCVD
.. autoattribute:: CONNECTION_CLOSED_ABNORMALLY
.. autoattribute:: INVALID_DATA
.. autoattribute:: POLICY_VIOLATION
.. autoattribute:: MESSAGE_TOO_BIG
.. autoattribute:: MANDATORY_EXTENSION
.. autoattribute:: INTERNAL_ERROR
.. autoattribute:: SERVICE_RESTART
.. autoattribute:: TRY_AGAIN_LATER
.. autoattribute:: BAD_GATEWAY
.. autoattribute:: TLS_FAILURE

HTTP events
-----------

Expand Down
2 changes: 1 addition & 1 deletion docs/topics/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ connection:
token = await websocket.recv()
user = get_user(token)
if user is None:
await websocket.close(1011, "authentication failed")
await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
return
...
Expand Down
3 changes: 2 additions & 1 deletion example/django/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
django.setup()

from sesame.utils import get_user
from websockets.frames import CloseCode


async def handler(websocket):
sesame = await websocket.recv()
user = await asyncio.to_thread(get_user, sesame)
if user is None:
await websocket.close(1011, "authentication failed")
await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
return

await websocket.send(f"Hello {user}!")
Expand Down
3 changes: 2 additions & 1 deletion example/django/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from django.contrib.contenttypes.models import ContentType
from sesame.utils import get_user
from websockets.frames import CloseCode


CONNECTIONS = {}
Expand All @@ -33,7 +34,7 @@ async def handler(websocket):
sesame = await websocket.recv()
user = await asyncio.to_thread(get_user, sesame)
if user is None:
await websocket.close(1011, "authentication failed")
await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
return

ct_ids = await asyncio.to_thread(get_content_types, user)
Expand Down
3 changes: 2 additions & 1 deletion experiments/authentication/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import uuid

import websockets
from websockets.frames import CloseCode


# User accounts database
Expand Down Expand Up @@ -95,7 +96,7 @@ async def first_message_handler(websocket):
token = await websocket.recv()
user = get_user(token)
if user is None:
await websocket.close(1011, "authentication failed")
await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
return

await websocket.send(f"Hello {user}!")
Expand Down
2 changes: 1 addition & 1 deletion src/websockets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def __str__(self) -> str:
@property
def code(self) -> int:
if self.rcvd is None:
return 1006
return frames.CloseCode.ABNORMAL_CLOSURE
return self.rcvd.code

@property
Expand Down
89 changes: 55 additions & 34 deletions src/websockets/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,48 +52,69 @@ class Opcode(enum.IntEnum):
CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG


# See https://www.iana.org/assignments/websocket/websocket.xhtml
CLOSE_CODES = {
1000: "OK",
1001: "going away",
1002: "protocol error",
1003: "unsupported type",
class CloseCode(enum.IntEnum):
"""Close code values for WebSocket close frames."""

NORMAL_CLOSURE = 1000
GOING_AWAY = 1001
PROTOCOL_ERROR = 1002
UNSUPPORTED_DATA = 1003
# 1004 is reserved
1005: "no status code [internal]",
1006: "connection closed abnormally [internal]",
1007: "invalid data",
1008: "policy violation",
1009: "message too big",
1010: "extension required",
1011: "unexpected error",
1012: "service restart",
1013: "try again later",
1014: "bad gateway",
1015: "TLS failure [internal]",
NO_STATUS_RCVD = 1005
ABNORMAL_CLOSURE = 1006
INVALID_DATA = 1007
POLICY_VIOLATION = 1008
MESSAGE_TOO_BIG = 1009
MANDATORY_EXTENSION = 1010
INTERNAL_ERROR = 1011
SERVICE_RESTART = 1012
TRY_AGAIN_LATER = 1013
BAD_GATEWAY = 1014
TLS_HANDSHAKE = 1015


# See https://www.iana.org/assignments/websocket/websocket.xhtml
CLOSE_CODE_EXPLANATIONS: dict[int, str] = {
CloseCode.NORMAL_CLOSURE: "OK",
CloseCode.GOING_AWAY: "going away",
CloseCode.PROTOCOL_ERROR: "protocol error",
CloseCode.UNSUPPORTED_DATA: "unsupported data",
CloseCode.NO_STATUS_RCVD: "no status received [internal]",
CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]",
CloseCode.INVALID_DATA: "invalid frame payload data",
CloseCode.POLICY_VIOLATION: "policy violation",
CloseCode.MESSAGE_TOO_BIG: "message too big",
CloseCode.MANDATORY_EXTENSION: "mandatory extension",
CloseCode.INTERNAL_ERROR: "internal error",
CloseCode.SERVICE_RESTART: "service restart",
CloseCode.TRY_AGAIN_LATER: "try again later",
CloseCode.BAD_GATEWAY: "bad gateway",
CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]",
}


# Close code that are allowed in a close frame.
# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`.
EXTERNAL_CLOSE_CODES = {
1000,
1001,
1002,
1003,
1007,
1008,
1009,
1010,
1011,
1012,
1013,
1014,
CloseCode.NORMAL_CLOSURE,
CloseCode.GOING_AWAY,
CloseCode.PROTOCOL_ERROR,
CloseCode.UNSUPPORTED_DATA,
CloseCode.INVALID_DATA,
CloseCode.POLICY_VIOLATION,
CloseCode.MESSAGE_TOO_BIG,
CloseCode.MANDATORY_EXTENSION,
CloseCode.INTERNAL_ERROR,
CloseCode.SERVICE_RESTART,
CloseCode.TRY_AGAIN_LATER,
CloseCode.BAD_GATEWAY,
}


OK_CLOSE_CODES = {
1000,
1001,
1005,
CloseCode.NORMAL_CLOSURE,
CloseCode.GOING_AWAY,
CloseCode.NO_STATUS_RCVD,
}


Expand Down Expand Up @@ -397,7 +418,7 @@ def __str__(self) -> str:
elif 4000 <= self.code < 5000:
explanation = "private use"
else:
explanation = CLOSE_CODES.get(self.code, "unknown")
explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown")
result = f"{self.code} ({explanation})"

if self.reason:
Expand Down Expand Up @@ -425,7 +446,7 @@ def parse(cls, data: bytes) -> Close:
close.check()
return close
elif len(data) == 0:
return cls(1005, "")
return cls(CloseCode.NO_STATUS_RCVD, "")
else:
raise exceptions.ProtocolError("close frame too short")

Expand Down

0 comments on commit 2b627b2

Please sign in to comment.