0
# Exception Handling
1
2
Comprehensive exception hierarchy for precise error handling covering connection lifecycle, protocol violations, handshake failures, validation errors, and network issues in WebSocket operations.
3
4
## Capabilities
5
6
### Base Exception Classes
7
8
Foundation exception classes that serve as the root of the WebSocket exception hierarchy.
9
10
```python { .api }
11
class WebSocketException(Exception):
12
"""
13
Base class for all WebSocket-specific exceptions.
14
15
All other WebSocket exceptions inherit from this class,
16
allowing catch-all exception handling for WebSocket operations.
17
"""
18
19
def __init__(self, message: str = None):
20
"""
21
Initialize WebSocket exception.
22
23
Parameters:
24
- message: Human-readable error description
25
"""
26
super().__init__(message)
27
```
28
29
### Connection Lifecycle Exceptions
30
31
Exceptions related to WebSocket connection establishment, maintenance, and closure.
32
33
```python { .api }
34
class ConnectionClosed(WebSocketException):
35
"""
36
Base exception for closed WebSocket connections.
37
38
Raised when attempting operations on a closed connection
39
or when connection closure is detected.
40
"""
41
42
def __init__(self, rcvd: Close = None, sent: Close = None):
43
"""
44
Initialize connection closed exception.
45
46
Parameters:
47
- rcvd: Close frame received from peer (if any)
48
- sent: Close frame sent to peer (if any)
49
"""
50
self.rcvd = rcvd
51
self.sent = sent
52
53
if rcvd:
54
super().__init__(f"Connection closed: {rcvd.code} {rcvd.reason}")
55
else:
56
super().__init__("Connection closed")
57
58
@property
59
def code(self) -> int | None:
60
"""Get close code from received close frame."""
61
return self.rcvd.code if self.rcvd else None
62
63
@property
64
def reason(self) -> str | None:
65
"""Get close reason from received close frame."""
66
return self.rcvd.reason if self.rcvd else None
67
68
class ConnectionClosedOK(ConnectionClosed):
69
"""
70
Normal WebSocket connection closure (close code 1000).
71
72
Raised when connection is closed normally without errors,
73
typically when client or server explicitly closes connection.
74
"""
75
pass
76
77
class ConnectionClosedError(ConnectionClosed):
78
"""
79
Abnormal WebSocket connection closure (close code != 1000).
80
81
Raised when connection is closed due to errors, protocol violations,
82
or unexpected network conditions.
83
"""
84
pass
85
```
86
87
### Handshake Exceptions
88
89
Exceptions that occur during the WebSocket handshake process, including validation and negotiation failures.
90
91
```python { .api }
92
class InvalidHandshake(WebSocketException):
93
"""
94
Base exception for WebSocket handshake failures.
95
96
Raised when handshake process fails due to invalid headers,
97
unsupported protocols, or security policy violations.
98
"""
99
pass
100
101
class SecurityError(InvalidHandshake):
102
"""
103
Security policy violation during handshake.
104
105
Raised when connection attempt violates security policies
106
such as origin restrictions or certificate validation.
107
"""
108
pass
109
110
class InvalidMessage(InvalidHandshake):
111
"""
112
Malformed handshake message.
113
114
Raised when HTTP handshake message is malformed or
115
doesn't conform to WebSocket upgrade requirements.
116
"""
117
pass
118
119
class InvalidStatus(InvalidHandshake):
120
"""
121
WebSocket upgrade rejection.
122
123
Raised when server rejects WebSocket upgrade request
124
with non-101 HTTP status code.
125
"""
126
127
def __init__(self, status_code: int, headers: Headers = None):
128
"""
129
Initialize invalid status exception.
130
131
Parameters:
132
- status_code: HTTP status code received
133
- headers: HTTP response headers
134
"""
135
self.status_code = status_code
136
self.headers = headers or Headers()
137
super().__init__(f"HTTP {status_code}")
138
139
class InvalidHeader(InvalidHandshake):
140
"""
141
Invalid HTTP header in handshake.
142
143
Raised when handshake contains invalid or missing
144
required HTTP headers.
145
"""
146
147
def __init__(self, name: str, value: str = None):
148
"""
149
Initialize invalid header exception.
150
151
Parameters:
152
- name: Header name that is invalid
153
- value: Header value (if applicable)
154
"""
155
self.name = name
156
self.value = value
157
158
if value:
159
super().__init__(f"Invalid header {name}: {value}")
160
else:
161
super().__init__(f"Invalid header: {name}")
162
163
class InvalidHeaderFormat(InvalidHeader):
164
"""
165
Header parsing failure.
166
167
Raised when HTTP header cannot be parsed due to
168
incorrect format or encoding issues.
169
"""
170
pass
171
172
class InvalidHeaderValue(InvalidHeader):
173
"""
174
Semantically invalid header value.
175
176
Raised when header value is syntactically valid but
177
semantically incorrect for WebSocket handshake.
178
"""
179
pass
180
181
class InvalidOrigin(InvalidHandshake):
182
"""
183
Disallowed origin header.
184
185
Raised when Origin header doesn't match server's
186
allowed origins policy.
187
"""
188
189
def __init__(self, origin: str):
190
"""
191
Initialize invalid origin exception.
192
193
Parameters:
194
- origin: The disallowed origin value
195
"""
196
self.origin = origin
197
super().__init__(f"Invalid origin: {origin}")
198
199
class InvalidUpgrade(InvalidHandshake):
200
"""
201
Incorrect WebSocket upgrade headers.
202
203
Raised when required WebSocket upgrade headers
204
are missing or have incorrect values.
205
"""
206
pass
207
208
class NegotiationError(InvalidHandshake):
209
"""
210
Extension or subprotocol negotiation failure.
211
212
Raised when client and server cannot agree on
213
WebSocket extensions or subprotocols.
214
"""
215
pass
216
```
217
218
### Extension and Parameter Exceptions
219
220
Exceptions related to WebSocket extension processing and parameter validation.
221
222
```python { .api }
223
class DuplicateParameter(InvalidHandshake):
224
"""
225
Duplicate extension parameter.
226
227
Raised when WebSocket extension contains
228
duplicate parameter names.
229
"""
230
231
def __init__(self, name: str):
232
"""
233
Initialize duplicate parameter exception.
234
235
Parameters:
236
- name: Parameter name that is duplicated
237
"""
238
self.name = name
239
super().__init__(f"Duplicate parameter: {name}")
240
241
class InvalidParameterName(InvalidHandshake):
242
"""
243
Invalid extension parameter name.
244
245
Raised when extension parameter name doesn't
246
conform to specification requirements.
247
"""
248
249
def __init__(self, name: str):
250
"""
251
Initialize invalid parameter name exception.
252
253
Parameters:
254
- name: Invalid parameter name
255
"""
256
self.name = name
257
super().__init__(f"Invalid parameter name: {name}")
258
259
class InvalidParameterValue(InvalidHandshake):
260
"""
261
Invalid extension parameter value.
262
263
Raised when extension parameter value is
264
invalid for the specific parameter.
265
"""
266
267
def __init__(self, name: str, value: str):
268
"""
269
Initialize invalid parameter value exception.
270
271
Parameters:
272
- name: Parameter name
273
- value: Invalid parameter value
274
"""
275
self.name = name
276
self.value = value
277
super().__init__(f"Invalid parameter value {name}: {value}")
278
```
279
280
### Network and URI Exceptions
281
282
Exceptions related to network connectivity, proxy handling, and URI validation.
283
284
```python { .api }
285
class InvalidURI(WebSocketException):
286
"""
287
Invalid WebSocket URI format.
288
289
Raised when WebSocket URI is malformed or
290
uses unsupported scheme or format.
291
"""
292
293
def __init__(self, uri: str, reason: str = None):
294
"""
295
Initialize invalid URI exception.
296
297
Parameters:
298
- uri: The invalid URI
299
- reason: Specific reason for invalidity
300
"""
301
self.uri = uri
302
self.reason = reason
303
304
if reason:
305
super().__init__(f"Invalid URI {uri}: {reason}")
306
else:
307
super().__init__(f"Invalid URI: {uri}")
308
309
class ProxyError(InvalidHandshake):
310
"""
311
Proxy connection failure.
312
313
Raised when connection through HTTP/SOCKS proxy
314
fails or proxy rejects the request.
315
"""
316
pass
317
318
class InvalidProxy(ProxyError):
319
"""
320
Invalid proxy configuration.
321
322
Raised when proxy settings are malformed or
323
proxy server information is invalid.
324
"""
325
pass
326
327
class InvalidProxyMessage(ProxyError):
328
"""
329
Malformed proxy response.
330
331
Raised when proxy server returns malformed
332
HTTP response during connection establishment.
333
"""
334
pass
335
336
class InvalidProxyStatus(ProxyError):
337
"""
338
Proxy rejection or error status.
339
340
Raised when proxy server rejects connection
341
request with error status code.
342
"""
343
344
def __init__(self, status_code: int, reason: str = None):
345
"""
346
Initialize invalid proxy status exception.
347
348
Parameters:
349
- status_code: HTTP status code from proxy
350
- reason: Optional reason phrase
351
"""
352
self.status_code = status_code
353
self.reason = reason
354
355
if reason:
356
super().__init__(f"Proxy error {status_code}: {reason}")
357
else:
358
super().__init__(f"Proxy error: {status_code}")
359
```
360
361
### Protocol Violations
362
363
Exceptions for WebSocket protocol violations and operational errors.
364
365
```python { .api }
366
class ProtocolError(WebSocketException):
367
"""
368
WebSocket protocol violation.
369
370
Raised when received data violates WebSocket protocol
371
specification or when invalid operations are attempted.
372
"""
373
pass
374
375
class PayloadTooBig(ProtocolError):
376
"""
377
Message exceeds size limits.
378
379
Raised when received message or frame exceeds
380
configured maximum size limits.
381
"""
382
383
def __init__(self, size: int, max_size: int):
384
"""
385
Initialize payload too big exception.
386
387
Parameters:
388
- size: Actual payload size
389
- max_size: Maximum allowed size
390
"""
391
self.size = size
392
self.max_size = max_size
393
super().__init__(f"Payload too big: {size} > {max_size}")
394
395
class InvalidState(ProtocolError):
396
"""
397
Invalid operation for current connection state.
398
399
Raised when attempting operations that are not
400
valid for the current WebSocket connection state.
401
"""
402
403
def __init__(self, operation: str, state: str):
404
"""
405
Initialize invalid state exception.
406
407
Parameters:
408
- operation: Operation that was attempted
409
- state: Current connection state
410
"""
411
self.operation = operation
412
self.state = state
413
super().__init__(f"Cannot {operation} in state {state}")
414
415
class ConcurrencyError(ProtocolError):
416
"""
417
Concurrent read/write operations detected.
418
419
Raised when multiple coroutines attempt to read from
420
or write to the same WebSocket connection simultaneously.
421
"""
422
423
def __init__(self, operation: str):
424
"""
425
Initialize concurrency error exception.
426
427
Parameters:
428
- operation: Operation that detected concurrency issue
429
"""
430
self.operation = operation
431
super().__init__(f"Concurrent {operation} operations")
432
```
433
434
## Usage Examples
435
436
### Basic Exception Handling
437
438
```python
439
import asyncio
440
from websockets import connect, ConnectionClosed, InvalidURI, ProtocolError
441
442
async def basic_exception_handling():
443
"""Demonstrate basic WebSocket exception handling."""
444
try:
445
async with connect("ws://localhost:8765") as websocket:
446
# Send and receive messages
447
await websocket.send("Hello, Server!")
448
response = await websocket.recv()
449
print(f"Received: {response}")
450
451
except ConnectionClosed as e:
452
print(f"Connection closed: code={e.code}, reason={e.reason}")
453
454
# Check if closure was normal or error
455
if isinstance(e, ConnectionClosedOK):
456
print("Connection closed normally")
457
elif isinstance(e, ConnectionClosedError):
458
print("Connection closed with error")
459
460
except InvalidURI as e:
461
print(f"Invalid WebSocket URI: {e.uri}")
462
if e.reason:
463
print(f"Reason: {e.reason}")
464
465
except ProtocolError as e:
466
print(f"Protocol error: {e}")
467
468
except Exception as e:
469
print(f"Unexpected error: {e}")
470
471
asyncio.run(basic_exception_handling())
472
```
473
474
### Handshake Exception Handling
475
476
```python
477
import asyncio
478
from websockets import connect
479
from websockets import (
480
InvalidHandshake, SecurityError, InvalidStatus, InvalidOrigin,
481
NegotiationError, InvalidHeader
482
)
483
484
async def handshake_exception_handling():
485
"""Handle various handshake failures."""
486
test_cases = [
487
("ws://localhost:8765", None), # Normal case
488
("ws://invalid-server:9999", "Connection failed"),
489
("wss://expired-cert.example.com", "Certificate error"),
490
]
491
492
for uri, expected_error in test_cases:
493
try:
494
print(f"Connecting to {uri}...")
495
496
async with connect(
497
uri,
498
additional_headers={"Origin": "https://myapp.com"},
499
subprotocols=["chat", "notifications"],
500
open_timeout=5
501
) as websocket:
502
await websocket.send("Connection successful")
503
response = await websocket.recv()
504
print(f"Success: {response}")
505
506
except SecurityError as e:
507
print(f"Security error: {e}")
508
509
except InvalidStatus as e:
510
print(f"Server rejected connection: HTTP {e.status_code}")
511
print(f"Response headers: {dict(e.headers)}")
512
513
except InvalidOrigin as e:
514
print(f"Origin rejected: {e.origin}")
515
516
except InvalidHeader as e:
517
print(f"Invalid header: {e.name} = {e.value}")
518
519
except NegotiationError as e:
520
print(f"Failed to negotiate extensions/subprotocols: {e}")
521
522
except InvalidHandshake as e:
523
print(f"Handshake failed: {e}")
524
525
except Exception as e:
526
print(f"Other error: {e}")
527
528
print()
529
530
asyncio.run(handshake_exception_handling())
531
```
532
533
### Connection State Exception Handling
534
535
```python
536
import asyncio
537
from websockets import connect, InvalidState, ConcurrencyError, PayloadTooBig
538
539
async def state_exception_handling():
540
"""Handle connection state and operational errors."""
541
try:
542
async with connect(
543
"ws://localhost:8765",
544
max_size=1024 # 1KB max message size
545
) as websocket:
546
547
# Test large message (should raise PayloadTooBig)
548
try:
549
large_message = "x" * 2048 # 2KB message
550
await websocket.send(large_message)
551
except PayloadTooBig as e:
552
print(f"Message too large: {e.size} > {e.max_size}")
553
554
# Test concurrent operations
555
try:
556
# This would cause concurrency error if attempted
557
# await asyncio.gather(
558
# websocket.recv(),
559
# websocket.recv() # Concurrent recv operations
560
# )
561
pass
562
except ConcurrencyError as e:
563
print(f"Concurrency error: {e.operation}")
564
565
# Normal operation
566
await websocket.send("Hello")
567
response = await websocket.recv()
568
print(f"Normal operation: {response}")
569
570
# Test operation after close
571
await websocket.close()
572
573
try:
574
await websocket.send("This should fail")
575
except InvalidState as e:
576
print(f"Invalid state: {e.operation} in {e.state}")
577
578
except Exception as e:
579
print(f"Connection error: {e}")
580
581
asyncio.run(state_exception_handling())
582
```
583
584
### Robust Error Recovery
585
586
```python
587
import asyncio
588
import logging
589
from websockets import connect, WebSocketException, ConnectionClosed
590
591
# Set up logging
592
logging.basicConfig(level=logging.INFO)
593
logger = logging.getLogger(__name__)
594
595
async def robust_client_with_recovery():
596
"""Client with comprehensive error handling and recovery."""
597
uri = "ws://localhost:8765"
598
max_retries = 3
599
retry_delay = 1
600
601
for attempt in range(max_retries):
602
try:
603
logger.info(f"Connection attempt {attempt + 1}")
604
605
async with connect(uri, ping_interval=20, ping_timeout=10) as websocket:
606
logger.info("Connected successfully")
607
608
# Main message loop with error recovery
609
message_count = 0
610
error_count = 0
611
612
while error_count < 5: # Max 5 consecutive errors
613
try:
614
# Send periodic messages
615
message = f"Message {message_count}"
616
await websocket.send(message)
617
message_count += 1
618
619
# Wait for response with timeout
620
try:
621
response = await asyncio.wait_for(
622
websocket.recv(),
623
timeout=30
624
)
625
logger.info(f"Received: {response}")
626
error_count = 0 # Reset error count on success
627
628
except asyncio.TimeoutError:
629
logger.warning("Response timeout")
630
error_count += 1
631
continue
632
633
# Wait before next message
634
await asyncio.sleep(5)
635
636
except ConnectionClosed as e:
637
logger.warning(f"Connection closed: {e.code} {e.reason}")
638
break
639
640
except WebSocketException as e:
641
logger.error(f"WebSocket error: {e}")
642
error_count += 1
643
644
if error_count < 5:
645
await asyncio.sleep(1) # Brief pause before retry
646
647
except Exception as e:
648
logger.error(f"Unexpected error: {e}")
649
error_count += 1
650
651
if error_count >= 5:
652
logger.error("Too many consecutive errors, giving up")
653
break
654
655
except WebSocketException as e:
656
logger.error(f"WebSocket connection failed: {e}")
657
658
except Exception as e:
659
logger.error(f"Unexpected connection error: {e}")
660
661
# Retry logic
662
if attempt < max_retries - 1:
663
logger.info(f"Retrying in {retry_delay} seconds...")
664
await asyncio.sleep(retry_delay)
665
retry_delay *= 2 # Exponential backoff
666
else:
667
logger.error("Max retries exceeded")
668
669
asyncio.run(robust_client_with_recovery())
670
```
671
672
### Exception Classification and Logging
673
674
```python
675
import asyncio
676
import logging
677
from websockets import connect
678
from websockets import (
679
WebSocketException, ConnectionClosed, InvalidHandshake, ProtocolError,
680
SecurityError, InvalidURI, ProxyError
681
)
682
683
class WebSocketErrorHandler:
684
"""Centralized WebSocket error handling and logging."""
685
686
def __init__(self):
687
self.logger = logging.getLogger(self.__class__.__name__)
688
689
def classify_error(self, exception: Exception) -> str:
690
"""Classify exception into error categories."""
691
if isinstance(exception, ConnectionClosed):
692
return "connection_closed"
693
elif isinstance(exception, SecurityError):
694
return "security_error"
695
elif isinstance(exception, InvalidHandshake):
696
return "handshake_error"
697
elif isinstance(exception, ProtocolError):
698
return "protocol_error"
699
elif isinstance(exception, ProxyError):
700
return "proxy_error"
701
elif isinstance(exception, InvalidURI):
702
return "uri_error"
703
elif isinstance(exception, WebSocketException):
704
return "websocket_error"
705
else:
706
return "unknown_error"
707
708
def should_retry(self, exception: Exception) -> bool:
709
"""Determine if operation should be retried."""
710
category = self.classify_error(exception)
711
712
# Don't retry on these error types
713
no_retry_categories = {
714
"security_error", "uri_error", "handshake_error"
715
}
716
717
return category not in no_retry_categories
718
719
def log_error(self, exception: Exception, context: str = ""):
720
"""Log error with appropriate level and details."""
721
category = self.classify_error(exception)
722
723
if category == "connection_closed":
724
if isinstance(exception, ConnectionClosedOK):
725
self.logger.info(f"{context}: Connection closed normally")
726
else:
727
self.logger.warning(f"{context}: {exception}")
728
729
elif category in ["security_error", "protocol_error"]:
730
self.logger.error(f"{context}: {category}: {exception}")
731
732
elif category in ["handshake_error", "proxy_error"]:
733
self.logger.warning(f"{context}: {category}: {exception}")
734
735
else:
736
self.logger.error(f"{context}: {category}: {exception}")
737
738
async def error_handling_example():
739
"""Demonstrate centralized error handling."""
740
error_handler = WebSocketErrorHandler()
741
742
test_uris = [
743
"ws://localhost:8765",
744
"ws://invalid-host:9999",
745
"wss://expired-cert.example.com",
746
"invalid-uri",
747
]
748
749
for uri in test_uris:
750
try:
751
context = f"Connecting to {uri}"
752
753
async with connect(uri, open_timeout=5) as websocket:
754
await websocket.send("Test message")
755
response = await websocket.recv()
756
print(f"Success: {response}")
757
758
except Exception as e:
759
error_handler.log_error(e, context)
760
761
if error_handler.should_retry(e):
762
print(f"Error is retryable: {error_handler.classify_error(e)}")
763
else:
764
print(f"Error is not retryable: {error_handler.classify_error(e)}")
765
766
# Set up logging
767
logging.basicConfig(
768
level=logging.INFO,
769
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
770
)
771
772
asyncio.run(error_handling_example())
773
```