Skip to content

Commit 8ecc1c7

Browse files
committed
Support separate limits for messages and fragments.
Fix #1602.
1 parent 77cf665 commit 8ecc1c7

File tree

12 files changed

+94
-49
lines changed

12 files changed

+94
-49
lines changed

docs/faq/misc.rst

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ you must disable:
2222

2323
* Compression: set ``compression=None``
2424
* Keepalive: set ``ping_interval=None``
25+
* Limits: set ``max_size=None``
2526
* UTF-8 decoding: send ``bytes`` rather than ``str``
2627

2728
Then, please consider whether websockets is the bottleneck of the performance

docs/project/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ notice.
3535
Improvements
3636
............
3737

38+
* Allowed setting separate limits for messages and fragments with ``max_size``.
3839
* Added support for HTTP/1.0 proxies.
3940

4041
15.0.1

src/websockets/asyncio/client.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,9 @@ class connect:
236236
close_timeout: Timeout for closing the connection in seconds.
237237
:obj:`None` disables the timeout.
238238
max_size: Maximum size of incoming messages in bytes.
239-
:obj:`None` disables the limit.
239+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
240+
max_fragment_size)`` tuple to set different limits for messages and
241+
fragments when you expect long messages sent in short fragments.
240242
max_queue: High-water mark of the buffer where frames are received.
241243
It defaults to 16 frames. The low-water mark defaults to ``max_queue
242244
// 4``. You may pass a ``(high, low)`` tuple to set the high-water
@@ -314,7 +316,7 @@ def __init__(
314316
ping_timeout: float | None = 20,
315317
close_timeout: float | None = 10,
316318
# Limits
317-
max_size: int | None = 2**20,
319+
max_size: int | None | tuple[int | None, int | None] = 2**20,
318320
max_queue: int | None | tuple[int | None, int | None] = 16,
319321
write_limit: int | tuple[int, int | None] = 2**15,
320322
# Logging

src/websockets/asyncio/connection.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ def __init__(
6464
self.ping_timeout = ping_timeout
6565
self.close_timeout = close_timeout
6666
if isinstance(max_queue, int) or max_queue is None:
67-
max_queue = (max_queue, None)
68-
self.max_queue = max_queue
67+
self.max_queue = (max_queue, None)
68+
else:
69+
self.max_queue = max_queue
6970
if isinstance(write_limit, int):
7071
write_limit = (write_limit, None)
7172
self.write_limit = write_limit

src/websockets/asyncio/server.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,9 @@ def handler(websocket):
644644
close_timeout: Timeout for closing connections in seconds.
645645
:obj:`None` disables the timeout.
646646
max_size: Maximum size of incoming messages in bytes.
647-
:obj:`None` disables the limit.
647+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
648+
max_fragment_size)`` tuple to set different limits for messages and
649+
fragments when you expect long messages sent in short fragments.
648650
max_queue: High-water mark of the buffer where frames are received.
649651
It defaults to 16 frames. The low-water mark defaults to ``max_queue
650652
// 4``. You may pass a ``(high, low)`` tuple to set the high-water
@@ -719,7 +721,7 @@ def __init__(
719721
ping_timeout: float | None = 20,
720722
close_timeout: float | None = 10,
721723
# Limits
722-
max_size: int | None = 2**20,
724+
max_size: int | None | tuple[int | None, int | None] = 2**20,
723725
max_queue: int | None | tuple[int | None, int | None] = 16,
724726
write_limit: int | tuple[int, int | None] = 2**15,
725727
# Logging

src/websockets/client.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ class ClientProtocol(Protocol):
6060
subprotocols: List of supported subprotocols, in order of decreasing
6161
preference.
6262
state: Initial state of the WebSocket connection.
63-
max_size: Maximum size of incoming messages in bytes;
64-
:obj:`None` disables the limit.
63+
max_size: Maximum size of incoming messages in bytes.
64+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
65+
max_fragment_size)`` tuple to set different limits for messages and
66+
fragments when you expect long messages sent in short fragments.
6567
logger: Logger for this connection;
6668
defaults to ``logging.getLogger("websockets.client")``;
6769
see the :doc:`logging guide <../../topics/logging>` for details.
@@ -76,7 +78,7 @@ def __init__(
7678
extensions: Sequence[ClientExtensionFactory] | None = None,
7779
subprotocols: Sequence[Subprotocol] | None = None,
7880
state: State = CONNECTING,
79-
max_size: int | None = 2**20,
81+
max_size: int | None | tuple[int | None, int | None] = 2**20,
8082
logger: LoggerLike | None = None,
8183
) -> None:
8284
super().__init__(

src/websockets/exceptions.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,11 @@ def __init__(
394394
self,
395395
size_or_message: int | None | str,
396396
max_size: int | None = None,
397-
cur_size: int | None = None,
397+
current_size: int | None = None,
398398
) -> None:
399399
if isinstance(size_or_message, str):
400400
assert max_size is None
401-
assert cur_size is None
401+
assert current_size is None
402402
warnings.warn( # deprecated in 14.0 - 2024-11-09
403403
"PayloadTooBig(message) is deprecated; "
404404
"change to PayloadTooBig(size, max_size)",
@@ -410,8 +410,8 @@ def __init__(
410410
self.size: int | None = size_or_message
411411
assert max_size is not None
412412
self.max_size: int = max_size
413-
self.cur_size: int | None = None
414-
self.set_current_size(cur_size)
413+
self.current_size: int | None = None
414+
self.set_current_size(current_size)
415415

416416
def __str__(self) -> str:
417417
if self.message is not None:
@@ -420,16 +420,16 @@ def __str__(self) -> str:
420420
message = "frame "
421421
if self.size is not None:
422422
message += f"with {self.size} bytes "
423-
if self.cur_size is not None:
424-
message += f"after reading {self.cur_size} bytes "
423+
if self.current_size is not None:
424+
message += f"after reading {self.current_size} bytes "
425425
message += f"exceeds limit of {self.max_size} bytes"
426426
return message
427427

428-
def set_current_size(self, cur_size: int | None) -> None:
429-
assert self.cur_size is None
430-
if cur_size is not None:
431-
self.max_size += cur_size
432-
self.cur_size = cur_size
428+
def set_current_size(self, current_size: int | None) -> None:
429+
assert self.current_size is None
430+
if current_size is not None:
431+
self.max_size += current_size
432+
self.current_size = current_size
433433

434434

435435
class InvalidState(WebSocketException, AssertionError):

src/websockets/protocol.py

+30-18
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ class Protocol:
7777
Args:
7878
side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`.
7979
state: Initial state of the WebSocket connection.
80-
max_size: Maximum size of incoming messages in bytes;
81-
:obj:`None` disables the limit.
80+
max_size: Maximum size of incoming messages in bytes.
81+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
82+
max_fragment_size)`` tuple to set different limits for messages and
83+
fragments when you expect long messages sent in short fragments.
8284
logger: Logger for this connection; depending on ``side``,
8385
defaults to ``logging.getLogger("websockets.client")``
8486
or ``logging.getLogger("websockets.server")``;
@@ -91,7 +93,7 @@ def __init__(
9193
side: Side,
9294
*,
9395
state: State = OPEN,
94-
max_size: int | None = 2**20,
96+
max_size: tuple[int | None, int | None] | int | None = 2**20,
9597
logger: LoggerLike | None = None,
9698
) -> None:
9799
# Unique identifier. For logs.
@@ -114,11 +116,14 @@ def __init__(
114116
self.state = state
115117

116118
# Maximum size of incoming messages in bytes.
117-
self.max_size = max_size
119+
if isinstance(max_size, int) or max_size is None:
120+
self.max_message_size, self.max_fragment_size = max_size, None
121+
else:
122+
self.max_message_size, self.max_fragment_size = max_size
118123

119124
# Current size of incoming message in bytes. Only set while reading a
120125
# fragmented message i.e. a data frames with the FIN bit not set.
121-
self.cur_size: int | None = None
126+
self.current_size: int | None = None
122127

123128
# True while sending a fragmented message i.e. a data frames with the
124129
# FIN bit not set.
@@ -578,12 +583,19 @@ def parse(self) -> Generator[None]:
578583
# connection isn't closed cleanly.
579584
raise EOFError("unexpected end of stream")
580585

581-
if self.max_size is None:
582-
max_size = None
583-
elif self.cur_size is None:
584-
max_size = self.max_size
585-
else:
586-
max_size = self.max_size - self.cur_size
586+
max_size = None
587+
588+
if self.max_message_size is not None:
589+
if self.current_size is None:
590+
max_size = self.max_message_size
591+
else:
592+
max_size = self.max_message_size - self.current_size
593+
594+
if self.max_fragment_size is not None:
595+
if max_size is None:
596+
max_size = self.max_fragment_size
597+
else:
598+
max_size = min(max_size, self.max_fragment_size)
587599

588600
# During a normal closure, execution ends here on the next
589601
# iteration of the loop after receiving a close frame. At
@@ -613,7 +625,7 @@ def parse(self) -> Generator[None]:
613625
self.parser_exc = exc
614626

615627
except PayloadTooBig as exc:
616-
exc.set_current_size(self.cur_size)
628+
exc.set_current_size(self.current_size)
617629
self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc))
618630
self.parser_exc = exc
619631

@@ -664,18 +676,18 @@ def recv_frame(self, frame: Frame) -> None:
664676
665677
"""
666678
if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY:
667-
if self.cur_size is not None:
679+
if self.current_size is not None:
668680
raise ProtocolError("expected a continuation frame")
669681
if not frame.fin:
670-
self.cur_size = len(frame.data)
682+
self.current_size = len(frame.data)
671683

672684
elif frame.opcode is OP_CONT:
673-
if self.cur_size is None:
685+
if self.current_size is None:
674686
raise ProtocolError("unexpected continuation frame")
675687
if frame.fin:
676-
self.cur_size = None
688+
self.current_size = None
677689
else:
678-
self.cur_size += len(frame.data)
690+
self.current_size += len(frame.data)
679691

680692
elif frame.opcode is OP_PING:
681693
# 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST
@@ -696,7 +708,7 @@ def recv_frame(self, frame: Frame) -> None:
696708
assert self.close_sent is not None
697709
self.close_rcvd_then_sent = False
698710

699-
if self.cur_size is not None:
711+
if self.current_size is not None:
700712
raise ProtocolError("incomplete fragmented message")
701713

702714
# 5.5.1 Close: "If an endpoint receives a Close frame and did

src/websockets/server.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ class ServerProtocol(Protocol):
6565
signature as the :meth:`select_subprotocol` method, including a
6666
:class:`ServerProtocol` instance as first argument.
6767
state: Initial state of the WebSocket connection.
68-
max_size: Maximum size of incoming messages in bytes;
69-
:obj:`None` disables the limit.
68+
max_size: Maximum size of incoming messages in bytes.
69+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
70+
max_fragment_size)`` tuple to set different limits for messages and
71+
fragments when you expect long messages sent in short fragments.
7072
logger: Logger for this connection;
7173
defaults to ``logging.getLogger("websockets.server")``;
7274
see the :doc:`logging guide <../../topics/logging>` for details.
@@ -87,7 +89,7 @@ def __init__(
8789
| None
8890
) = None,
8991
state: State = CONNECTING,
90-
max_size: int | None = 2**20,
92+
max_size: int | None | tuple[int | None, int | None] = 2**20,
9193
logger: LoggerLike | None = None,
9294
) -> None:
9395
super().__init__(

src/websockets/sync/client.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def connect(
150150
ping_timeout: float | None = 20,
151151
close_timeout: float | None = 10,
152152
# Limits
153-
max_size: int | None = 2**20,
153+
max_size: int | None | tuple[int | None, int | None] = 2**20,
154154
max_queue: int | None | tuple[int | None, int | None] = 16,
155155
# Logging
156156
logger: LoggerLike | None = None,
@@ -210,7 +210,9 @@ def connect(
210210
close_timeout: Timeout for closing the connection in seconds.
211211
:obj:`None` disables the timeout.
212212
max_size: Maximum size of incoming messages in bytes.
213-
:obj:`None` disables the limit.
213+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
214+
max_fragment_size)`` tuple to set different limits for messages and
215+
fragments when you expect long messages sent in short fragments.
214216
max_queue: High-water mark of the buffer where frames are received.
215217
It defaults to 16 frames. The low-water mark defaults to ``max_queue
216218
// 4``. You may pass a ``(high, low)`` tuple to set the high-water

src/websockets/sync/server.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def serve(
365365
ping_timeout: float | None = 20,
366366
close_timeout: float | None = 10,
367367
# Limits
368-
max_size: int | None = 2**20,
368+
max_size: int | None | tuple[int | None, int | None] = 2**20,
369369
max_queue: int | None | tuple[int | None, int | None] = 16,
370370
# Logging
371371
logger: LoggerLike | None = None,
@@ -450,7 +450,9 @@ def handler(websocket):
450450
close_timeout: Timeout for closing connections in seconds.
451451
:obj:`None` disables the timeout.
452452
max_size: Maximum size of incoming messages in bytes.
453-
:obj:`None` disables the limit.
453+
:obj:`None` disables the limit. You may pass a ``(max_message_size,
454+
max_fragment_size)`` tuple to set different limits for messages and
455+
fragments when you expect long messages sent in short fragments.
454456
max_queue: High-water mark of the buffer where frames are received.
455457
It defaults to 16 frames. The low-water mark defaults to ``max_queue
456458
// 4``. You may pass a ``(high, low)`` tuple to set the high-water

tests/test_protocol.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,26 @@ def test_server_receives_fragmented_text(self):
365365
Frame(OP_CONT, "😀".encode()[2:]),
366366
)
367367

368-
def test_client_receives_fragmented_text_over_size_limit(self):
369-
client = Protocol(CLIENT, max_size=3)
368+
def test_client_receives_fragmented_text_over_fragment_size_limit(self):
369+
client = Protocol(CLIENT, max_size=(None, 3))
370+
client.receive_data(b"\x01\x04\xf0\x9f\x98\x80")
371+
self.assertIsInstance(client.parser_exc, PayloadTooBig)
372+
self.assertEqual(
373+
str(client.parser_exc),
374+
"frame with 4 bytes exceeds limit of 3 bytes",
375+
)
376+
377+
def test_server_receives_fragmented_text_over_fragment_size_limit(self):
378+
server = Protocol(SERVER, max_size=(None, 3))
379+
server.receive_data(b"\x01\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
380+
self.assertIsInstance(server.parser_exc, PayloadTooBig)
381+
self.assertEqual(
382+
str(server.parser_exc),
383+
"frame with 4 bytes exceeds limit of 3 bytes",
384+
)
385+
386+
def test_client_receives_fragmented_text_over_message_size_limit(self):
387+
client = Protocol(CLIENT, max_size=(3, 2))
370388
client.receive_data(b"\x01\x02\xf0\x9f")
371389
self.assertFrameReceived(
372390
client,
@@ -384,8 +402,8 @@ def test_client_receives_fragmented_text_over_size_limit(self):
384402
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
385403
)
386404

387-
def test_server_receives_fragmented_text_over_size_limit(self):
388-
server = Protocol(SERVER, max_size=3)
405+
def test_server_receives_fragmented_text_over_message_size_limit(self):
406+
server = Protocol(SERVER, max_size=(3, 2))
389407
server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
390408
self.assertFrameReceived(
391409
server,

0 commit comments

Comments
 (0)