Skip to content

Commit 9fcd8fd

Browse files
committed
Improve documentation of exceptions.
Group and order them. Extend docstrings.
1 parent 76b6844 commit 9fcd8fd

File tree

4 files changed

+114
-77
lines changed

4 files changed

+114
-77
lines changed

docs/project/changelog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ Bug fixes
613613
* Aligned maximum cookie size with popular web browsers.
614614

615615
* Ensured cancellation always propagates, even on Python versions where
616-
:exc:`~asyncio.CancelledError` inherits :exc:`Exception`.
616+
:exc:`~asyncio.CancelledError` inherits from :exc:`Exception`.
617617

618618
.. _8.1:
619619

docs/reference/exceptions.rst

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,35 @@ Exceptions
55

66
.. autoexception:: WebSocketException
77

8+
Connection closed
9+
-----------------
10+
11+
:meth:`~websockets.asyncio.connection.Connection.recv`,
12+
:meth:`~websockets.asyncio.connection.Connection.send`, and similar methods
13+
raise the exceptions below when the connection is closed. This is the expected
14+
way to detect disconnections.
15+
816
.. autoexception:: ConnectionClosed
917

18+
.. autoexception:: ConnectionClosedOK
19+
1020
.. autoexception:: ConnectionClosedError
1121

12-
.. autoexception:: ConnectionClosedOK
22+
Connection failed
23+
-----------------
24+
25+
These exceptions are raised by :func:`~websockets.asyncio.client.connect` when
26+
the opening handshake fails and the connection cannot be established. They are
27+
also reported by :func:`~websockets.asyncio.server.serve` in logs.
28+
29+
.. autoexception:: InvalidURI
1330

1431
.. autoexception:: InvalidHandshake
1532

1633
.. autoexception:: SecurityError
1734

35+
.. autoexception:: InvalidStatus
36+
1837
.. autoexception:: InvalidHeader
1938

2039
.. autoexception:: InvalidHeaderFormat
@@ -25,8 +44,6 @@ Exceptions
2544

2645
.. autoexception:: InvalidUpgrade
2746

28-
.. autoexception:: InvalidStatus
29-
3047
.. autoexception:: NegotiationError
3148

3249
.. autoexception:: DuplicateParameter
@@ -35,13 +52,17 @@ Exceptions
3552

3653
.. autoexception:: InvalidParameterValue
3754

38-
.. autoexception:: InvalidState
55+
Sans-I/O exceptions
56+
-------------------
3957

40-
.. autoexception:: InvalidURI
58+
These exceptions are only raised by the Sans-I/O implementation. They are
59+
translated to :exc:`ConnectionClosedError` in the other implementations.
60+
61+
.. autoexception:: ProtocolError
4162

4263
.. autoexception:: PayloadTooBig
4364

44-
.. autoexception:: ProtocolError
65+
.. autoexception:: InvalidState
4566

4667
Legacy exceptions
4768
-----------------

src/websockets/exceptions.py

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
"""
2-
:mod:`websockets.exceptions` defines the following exception hierarchy:
2+
:mod:`websockets.exceptions` defines the following hierarchy of exceptions.
33
44
* :exc:`WebSocketException`
55
* :exc:`ConnectionClosed`
6-
* :exc:`ConnectionClosedError`
76
* :exc:`ConnectionClosedOK`
7+
* :exc:`ConnectionClosedError`
8+
* :exc:`InvalidURI`
89
* :exc:`InvalidHandshake`
910
* :exc:`SecurityError`
1011
* :exc:`InvalidMessage` (legacy)
12+
* :exc:`InvalidStatus`
13+
* :exc:`InvalidStatusCode` (legacy)
1114
* :exc:`InvalidHeader`
1215
* :exc:`InvalidHeaderFormat`
1316
* :exc:`InvalidHeaderValue`
1417
* :exc:`InvalidOrigin`
1518
* :exc:`InvalidUpgrade`
16-
* :exc:`InvalidStatus`
17-
* :exc:`InvalidStatusCode` (legacy)
1819
* :exc:`NegotiationError`
1920
* :exc:`DuplicateParameter`
2021
* :exc:`InvalidParameterName`
2122
* :exc:`InvalidParameterValue`
2223
* :exc:`AbortHandshake` (legacy)
2324
* :exc:`RedirectHandshake` (legacy)
24-
* :exc:`InvalidState`
25-
* :exc:`InvalidURI`
26-
* :exc:`PayloadTooBig`
27-
* :exc:`ProtocolError`
25+
* :exc:`ProtocolError` (Sans-I/O)
26+
* :exc:`PayloadTooBig` (Sans-I/O)
27+
* :exc:`InvalidState` (Sans-I/O)
2828
2929
"""
3030

@@ -40,29 +40,29 @@
4040
__all__ = [
4141
"WebSocketException",
4242
"ConnectionClosed",
43-
"ConnectionClosedError",
4443
"ConnectionClosedOK",
44+
"ConnectionClosedError",
45+
"InvalidURI",
4546
"InvalidHandshake",
4647
"SecurityError",
4748
"InvalidMessage",
49+
"InvalidStatus",
50+
"InvalidStatusCode",
4851
"InvalidHeader",
4952
"InvalidHeaderFormat",
5053
"InvalidHeaderValue",
5154
"InvalidOrigin",
5255
"InvalidUpgrade",
53-
"InvalidStatus",
54-
"InvalidStatusCode",
5556
"NegotiationError",
5657
"DuplicateParameter",
5758
"InvalidParameterName",
5859
"InvalidParameterValue",
5960
"AbortHandshake",
6061
"RedirectHandshake",
61-
"InvalidState",
62-
"InvalidURI",
63-
"PayloadTooBig",
6462
"ProtocolError",
6563
"WebSocketProtocolError",
64+
"PayloadTooBig",
65+
"InvalidState",
6666
]
6767

6868

@@ -139,6 +139,16 @@ def reason(self) -> str:
139139
return self.rcvd.reason
140140

141141

142+
class ConnectionClosedOK(ConnectionClosed):
143+
"""
144+
Like :exc:`ConnectionClosed`, when the connection terminated properly.
145+
146+
A close code with code 1000 (OK) or 1001 (going away) or without a code was
147+
received and sent.
148+
149+
"""
150+
151+
142152
class ConnectionClosedError(ConnectionClosed):
143153
"""
144154
Like :exc:`ConnectionClosed`, when the connection terminated with an error.
@@ -149,19 +159,23 @@ class ConnectionClosedError(ConnectionClosed):
149159
"""
150160

151161

152-
class ConnectionClosedOK(ConnectionClosed):
162+
class InvalidURI(WebSocketException):
153163
"""
154-
Like :exc:`ConnectionClosed`, when the connection terminated properly.
155-
156-
A close code with code 1000 (OK) or 1001 (going away) or without a code was
157-
received and sent.
164+
Raised when connecting to a URI that isn't a valid WebSocket URI.
158165
159166
"""
160167

168+
def __init__(self, uri: str, msg: str) -> None:
169+
self.uri = uri
170+
self.msg = msg
171+
172+
def __str__(self) -> str:
173+
return f"{self.uri} isn't a valid URI: {self.msg}"
174+
161175

162176
class InvalidHandshake(WebSocketException):
163177
"""
164-
Raised during the handshake when the WebSocket connection fails.
178+
Base class for exceptions raised when the opening handshake fails.
165179
166180
"""
167181

@@ -170,10 +184,27 @@ class SecurityError(InvalidHandshake):
170184
"""
171185
Raised when a handshake request or response breaks a security rule.
172186
173-
Security limits are hard coded.
187+
Security limits can be configured with :doc:`environment variables
188+
<../reference/variables>`.
189+
190+
"""
191+
192+
193+
class InvalidStatus(InvalidHandshake):
194+
"""
195+
Raised when a handshake response rejects the WebSocket upgrade.
174196
175197
"""
176198

199+
def __init__(self, response: http11.Response) -> None:
200+
self.response = response
201+
202+
def __str__(self) -> str:
203+
return (
204+
"server rejected WebSocket connection: "
205+
f"HTTP {self.response.status_code:d}"
206+
)
207+
177208

178209
class InvalidHeader(InvalidHandshake):
179210
"""
@@ -210,7 +241,7 @@ class InvalidHeaderValue(InvalidHeader):
210241
"""
211242
Raised when an HTTP header has a wrong value.
212243
213-
The format of the header is correct but a value isn't acceptable.
244+
The format of the header is correct but the value isn't acceptable.
214245
215246
"""
216247

@@ -232,25 +263,9 @@ class InvalidUpgrade(InvalidHeader):
232263
"""
233264

234265

235-
class InvalidStatus(InvalidHandshake):
236-
"""
237-
Raised when a handshake response rejects the WebSocket upgrade.
238-
239-
"""
240-
241-
def __init__(self, response: http11.Response) -> None:
242-
self.response = response
243-
244-
def __str__(self) -> str:
245-
return (
246-
"server rejected WebSocket connection: "
247-
f"HTTP {self.response.status_code:d}"
248-
)
249-
250-
251266
class NegotiationError(InvalidHandshake):
252267
"""
253-
Raised when negotiating an extension fails.
268+
Raised when negotiating an extension or a subprotocol fails.
254269
255270
"""
256271

@@ -300,41 +315,42 @@ def __str__(self) -> str:
300315
return f"invalid value for parameter {self.name}: {self.value}"
301316

302317

303-
class InvalidState(WebSocketException, AssertionError):
318+
class ProtocolError(WebSocketException):
304319
"""
305-
Raised when an operation is forbidden in the current state.
320+
Raised when receiving or sending a frame that breaks the protocol.
306321
307-
This exception is an implementation detail.
322+
The Sans-I/O implementation raises this exception when:
308323
309-
It should never be raised in normal circumstances.
324+
* receiving or sending a frame that contains invalid data;
325+
* receiving or sending an invalid sequence of frames.
310326
311327
"""
312328

313329

314-
class InvalidURI(WebSocketException):
330+
class PayloadTooBig(WebSocketException):
315331
"""
316-
Raised when connecting to a URI that isn't a valid WebSocket URI.
332+
Raised when parsing a frame with a payload that exceeds the maximum size.
317333
318-
"""
319-
320-
def __init__(self, uri: str, msg: str) -> None:
321-
self.uri = uri
322-
self.msg = msg
323-
324-
def __str__(self) -> str:
325-
return f"{self.uri} isn't a valid URI: {self.msg}"
334+
The Sans-I/O layer uses this exception internally. It doesn't bubble up to
335+
the I/O layer.
326336
337+
The :meth:`~websockets.extensions.Extension.decode` method of extensions
338+
must raise :exc:`PayloadTooBig` if decoding a frame would exceed the limit.
327339
328-
class PayloadTooBig(WebSocketException):
329340
"""
330-
Raised when receiving a frame with a payload exceeding the maximum size.
331341

342+
343+
class InvalidState(WebSocketException, AssertionError):
332344
"""
345+
Raised when sending a frame is forbidden in the current state.
333346
347+
Specifically, the Sans-I/O layer raises this exception when:
334348
335-
class ProtocolError(WebSocketException):
336-
"""
337-
Raised when a frame breaks the protocol.
349+
* sending a data frame to a connection in a state other
350+
:attr:`~websockets.protocol.State.OPEN`;
351+
* sending a control frame to a connection in a state other than
352+
:attr:`~websockets.protocol.State.OPEN` or
353+
:attr:`~websockets.protocol.State.CLOSING`.
338354
339355
"""
340356

tests/test_exceptions.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ def test_str(self):
7979
),
8080
"no close frame received or sent",
8181
),
82+
(
83+
InvalidURI("|", "not at all!"),
84+
"| isn't a valid URI: not at all!",
85+
),
8286
(
8387
InvalidHandshake("invalid request"),
8488
"invalid request",
@@ -87,6 +91,10 @@ def test_str(self):
8791
SecurityError("redirect from WSS to WS"),
8892
"redirect from WSS to WS",
8993
),
94+
(
95+
InvalidStatus(Response(401, "Unauthorized", Headers())),
96+
"server rejected WebSocket connection: HTTP 401",
97+
),
9098
(
9199
InvalidHeader("Name"),
92100
"missing Name header",
@@ -123,10 +131,6 @@ def test_str(self):
123131
InvalidUpgrade("Connection", "websocket"),
124132
"invalid Connection header: websocket",
125133
),
126-
(
127-
InvalidStatus(Response(401, "Unauthorized", Headers())),
128-
"server rejected WebSocket connection: HTTP 401",
129-
),
130134
(
131135
NegotiationError("unsupported subprotocol: spam"),
132136
"unsupported subprotocol: spam",
@@ -152,20 +156,16 @@ def test_str(self):
152156
"invalid value for parameter a: |",
153157
),
154158
(
155-
InvalidState("WebSocket connection isn't established yet"),
156-
"WebSocket connection isn't established yet",
157-
),
158-
(
159-
InvalidURI("|", "not at all!"),
160-
"| isn't a valid URI: not at all!",
159+
ProtocolError("invalid opcode: 7"),
160+
"invalid opcode: 7",
161161
),
162162
(
163163
PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"),
164164
"payload length exceeds limit: 2 > 1 bytes",
165165
),
166166
(
167-
ProtocolError("invalid opcode: 7"),
168-
"invalid opcode: 7",
167+
InvalidState("WebSocket connection isn't established yet"),
168+
"WebSocket connection isn't established yet",
169169
),
170170
]:
171171
with self.subTest(exception=exception):

0 commit comments

Comments
 (0)