Skip to content

Commit 810bdeb

Browse files
committed
Report size correctly in PayloadTooBig.
Previously, it was reported incorrectly for fragmented messages. Fix #1522.
1 parent e44d795 commit 810bdeb

File tree

8 files changed

+143
-33
lines changed

8 files changed

+143
-33
lines changed

docs/project/changelog.rst

+22-9
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,6 @@ Backwards-incompatible changes
6767
Aliases for deprecated API were removed from ``__all__``. As a consequence,
6868
they cannot be imported e.g. with ``from websockets import *`` anymore.
6969

70-
.. admonition:: :attr:`Frame.data <frames.Frame.data>` is now a bytes-like object.
71-
:class: note
72-
73-
In addition to :class:`bytes`, it may be a :class:`bytearray` or a
74-
:class:`memoryview`.
75-
76-
If you wrote an :class:`extension <extensions.Extension>` that relies on
77-
methods not provided by these new types, you may need to update your code.
78-
7970
.. admonition:: Several API raise :exc:`ValueError` instead of :exc:`TypeError`
8071
on invalid arguments.
8172
:class: note
@@ -88,6 +79,26 @@ Backwards-incompatible changes
8879
raise :exc:`ValueError` when a required argument isn't provided or an
8980
argument that is incompatible with others is provided.
9081

82+
.. admonition:: :attr:`Frame.data <frames.Frame.data>` is now a bytes-like object.
83+
:class: note
84+
85+
In addition to :class:`bytes`, it may be a :class:`bytearray` or a
86+
:class:`memoryview`.
87+
88+
If you wrote an :class:`extension <extensions.Extension>` that relies on
89+
methods not provided by these new types, you may need to update your code.
90+
91+
.. admonition:: The signature of :exc:`~exceptions.PayloadTooBig` changed.
92+
:class: note
93+
94+
If you wrote an extension that raises :exc:`~exceptions.PayloadTooBig` in
95+
:meth:`~extensions.Extension.decode`, for example, you must replace::
96+
97+
PayloadTooBig(f"over size limit ({size} > {max_size} bytes)")
98+
99+
with::
100+
101+
PayloadTooBig(size, max_size)
91102

92103
New features
93104
............
@@ -105,6 +116,8 @@ Improvements
105116

106117
* Sending or receiving large compressed messages is now faster.
107118

119+
* Errors when a fragmented message is too large are clearer.
120+
108121
.. _13.1:
109122

110123
13.1

src/websockets/exceptions.py

+41
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,47 @@ class PayloadTooBig(WebSocketException):
334334
335335
"""
336336

337+
def __init__(
338+
self,
339+
size_or_message: int | None | str,
340+
max_size: int | None = None,
341+
cur_size: int | None = None,
342+
) -> None:
343+
if isinstance(size_or_message, str):
344+
assert max_size is None
345+
assert cur_size is None
346+
warnings.warn( # deprecated in 14.0
347+
"PayloadTooBig(message) is deprecated; "
348+
"change to PayloadTooBig(size, max_size)",
349+
DeprecationWarning,
350+
)
351+
self.message: str | None = size_or_message
352+
else:
353+
self.message = None
354+
self.size: int | None = size_or_message
355+
assert max_size is not None
356+
self.max_size: int = max_size
357+
self.cur_size: int | None = None
358+
self.set_current_size(cur_size)
359+
360+
def __str__(self) -> str:
361+
if self.message is not None:
362+
return self.message
363+
else:
364+
message = "frame "
365+
if self.size is not None:
366+
message += f"with {self.size} bytes "
367+
if self.cur_size is not None:
368+
message += f"after reading {self.cur_size} bytes "
369+
message += f"exceeds limit of {self.max_size} bytes"
370+
return message
371+
372+
def set_current_size(self, cur_size: int | None) -> None:
373+
assert self.cur_size is None
374+
if cur_size is not None:
375+
self.max_size += cur_size
376+
self.cur_size = cur_size
377+
337378

338379
class InvalidState(WebSocketException, AssertionError):
339380
"""

src/websockets/extensions/permessage_deflate.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def decode(
139139
try:
140140
data = self.decoder.decompress(data, max_length)
141141
if self.decoder.unconsumed_tail:
142-
raise PayloadTooBig(f"over size limit (? > {max_size} bytes)")
142+
assert max_size is not None # help mypy
143+
raise PayloadTooBig(None, max_size)
143144
if frame.fin and len(frame.data) >= 2044:
144145
# This cannot generate additional data.
145146
self.decoder.decompress(_EMPTY_UNCOMPRESSED_BLOCK)

src/websockets/frames.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def parse(
252252
data = yield from read_exact(8)
253253
(length,) = struct.unpack("!Q", data)
254254
if max_size is not None and length > max_size:
255-
raise PayloadTooBig(f"over size limit ({length} > {max_size} bytes)")
255+
raise PayloadTooBig(length, max_size)
256256
if mask:
257257
mask_bytes = yield from read_exact(4)
258258

src/websockets/legacy/framing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def read(
9393
data = await reader(8)
9494
(length,) = struct.unpack("!Q", data)
9595
if max_size is not None and length > max_size:
96-
raise PayloadTooBig(f"over size limit ({length} > {max_size} bytes)")
96+
raise PayloadTooBig(length, max_size)
9797
if mask:
9898
mask_bits = await reader(4)
9999

src/websockets/protocol.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ def parse(self) -> Generator[None]:
587587
self.parser_exc = exc
588588

589589
except PayloadTooBig as exc:
590+
exc.set_current_size(self.cur_size)
590591
self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc))
591592
self.parser_exc = exc
592593

@@ -639,9 +640,7 @@ def recv_frame(self, frame: Frame) -> None:
639640
if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY:
640641
if self.cur_size is not None:
641642
raise ProtocolError("expected a continuation frame")
642-
if frame.fin:
643-
self.cur_size = None
644-
else:
643+
if not frame.fin:
645644
self.cur_size = len(frame.data)
646645

647646
elif frame.opcode is OP_CONT:

tests/test_exceptions.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,16 @@ def test_str(self):
160160
"invalid opcode: 7",
161161
),
162162
(
163-
PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"),
164-
"payload length exceeds limit: 2 > 1 bytes",
163+
PayloadTooBig(None, 4),
164+
"frame exceeds limit of 4 bytes",
165+
),
166+
(
167+
PayloadTooBig(8, 4),
168+
"frame with 8 bytes exceeds limit of 4 bytes",
169+
),
170+
(
171+
PayloadTooBig(8, 4, 12),
172+
"frame with 8 bytes after reading 12 bytes exceeds limit of 16 bytes",
165173
),
166174
(
167175
InvalidState("WebSocket connection isn't established yet"),
@@ -202,3 +210,11 @@ def test_connection_closed_attributes_deprecation_defaults(self):
202210
"use Protocol.close_reason or ConnectionClosed.rcvd.reason"
203211
):
204212
self.assertEqual(exception.reason, "")
213+
214+
def test_payload_too_big_with_message(self):
215+
with self.assertDeprecationWarning(
216+
"PayloadTooBig(message) is deprecated; "
217+
"change to PayloadTooBig(size, max_size)",
218+
):
219+
exc = PayloadTooBig("payload length exceeds limit: 2 > 1 bytes")
220+
self.assertEqual(str(exc), "payload length exceeds limit: 2 > 1 bytes")

tests/test_protocol.py

+56-16
Original file line numberDiff line numberDiff line change
@@ -265,18 +265,28 @@ def test_client_receives_text_over_size_limit(self):
265265
client = Protocol(CLIENT, max_size=3)
266266
client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
267267
self.assertIsInstance(client.parser_exc, PayloadTooBig)
268-
self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)")
268+
self.assertEqual(
269+
str(client.parser_exc),
270+
"frame with 4 bytes exceeds limit of 3 bytes",
271+
)
269272
self.assertConnectionFailing(
270-
client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
273+
client,
274+
CloseCode.MESSAGE_TOO_BIG,
275+
"frame with 4 bytes exceeds limit of 3 bytes",
271276
)
272277

273278
def test_server_receives_text_over_size_limit(self):
274279
server = Protocol(SERVER, max_size=3)
275280
server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
276281
self.assertIsInstance(server.parser_exc, PayloadTooBig)
277-
self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)")
282+
self.assertEqual(
283+
str(server.parser_exc),
284+
"frame with 4 bytes exceeds limit of 3 bytes",
285+
)
278286
self.assertConnectionFailing(
279-
server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
287+
server,
288+
CloseCode.MESSAGE_TOO_BIG,
289+
"frame with 4 bytes exceeds limit of 3 bytes",
280290
)
281291

282292
def test_client_receives_text_without_size_limit(self):
@@ -363,9 +373,14 @@ def test_client_receives_fragmented_text_over_size_limit(self):
363373
)
364374
client.receive_data(b"\x80\x02\x98\x80")
365375
self.assertIsInstance(client.parser_exc, PayloadTooBig)
366-
self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)")
376+
self.assertEqual(
377+
str(client.parser_exc),
378+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
379+
)
367380
self.assertConnectionFailing(
368-
client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
381+
client,
382+
CloseCode.MESSAGE_TOO_BIG,
383+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
369384
)
370385

371386
def test_server_receives_fragmented_text_over_size_limit(self):
@@ -377,9 +392,14 @@ def test_server_receives_fragmented_text_over_size_limit(self):
377392
)
378393
server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
379394
self.assertIsInstance(server.parser_exc, PayloadTooBig)
380-
self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)")
395+
self.assertEqual(
396+
str(server.parser_exc),
397+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
398+
)
381399
self.assertConnectionFailing(
382-
server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
400+
server,
401+
CloseCode.MESSAGE_TOO_BIG,
402+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
383403
)
384404

385405
def test_client_receives_fragmented_text_without_size_limit(self):
@@ -533,18 +553,28 @@ def test_client_receives_binary_over_size_limit(self):
533553
client = Protocol(CLIENT, max_size=3)
534554
client.receive_data(b"\x82\x04\x01\x02\xfe\xff")
535555
self.assertIsInstance(client.parser_exc, PayloadTooBig)
536-
self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)")
556+
self.assertEqual(
557+
str(client.parser_exc),
558+
"frame with 4 bytes exceeds limit of 3 bytes",
559+
)
537560
self.assertConnectionFailing(
538-
client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
561+
client,
562+
CloseCode.MESSAGE_TOO_BIG,
563+
"frame with 4 bytes exceeds limit of 3 bytes",
539564
)
540565

541566
def test_server_receives_binary_over_size_limit(self):
542567
server = Protocol(SERVER, max_size=3)
543568
server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff")
544569
self.assertIsInstance(server.parser_exc, PayloadTooBig)
545-
self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)")
570+
self.assertEqual(
571+
str(server.parser_exc),
572+
"frame with 4 bytes exceeds limit of 3 bytes",
573+
)
546574
self.assertConnectionFailing(
547-
server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)"
575+
server,
576+
CloseCode.MESSAGE_TOO_BIG,
577+
"frame with 4 bytes exceeds limit of 3 bytes",
548578
)
549579

550580
def test_client_sends_fragmented_binary(self):
@@ -615,9 +645,14 @@ def test_client_receives_fragmented_binary_over_size_limit(self):
615645
)
616646
client.receive_data(b"\x80\x02\xfe\xff")
617647
self.assertIsInstance(client.parser_exc, PayloadTooBig)
618-
self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)")
648+
self.assertEqual(
649+
str(client.parser_exc),
650+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
651+
)
619652
self.assertConnectionFailing(
620-
client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
653+
client,
654+
CloseCode.MESSAGE_TOO_BIG,
655+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
621656
)
622657

623658
def test_server_receives_fragmented_binary_over_size_limit(self):
@@ -629,9 +664,14 @@ def test_server_receives_fragmented_binary_over_size_limit(self):
629664
)
630665
server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff")
631666
self.assertIsInstance(server.parser_exc, PayloadTooBig)
632-
self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)")
667+
self.assertEqual(
668+
str(server.parser_exc),
669+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
670+
)
633671
self.assertConnectionFailing(
634-
server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)"
672+
server,
673+
CloseCode.MESSAGE_TOO_BIG,
674+
"frame with 2 bytes after reading 2 bytes exceeds limit of 3 bytes",
635675
)
636676

637677
def test_client_sends_unexpected_binary(self):

0 commit comments

Comments
 (0)