Skip to content

Commit 1f06a69

Browse files
committed
Add an option to disable decoding of text frames.
Also support decoding binary frames. Fix #1376.
1 parent fbf69cc commit 1f06a69

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

src/websockets/asyncio/connection.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async def __aiter__(self) -> AsyncIterator[Data]:
172172
except ConnectionClosedOK:
173173
return
174174

175-
async def recv(self) -> Data:
175+
async def recv(self, decode: bool | None = None) -> Data:
176176
"""
177177
Receive the next message.
178178
@@ -192,21 +192,34 @@ async def recv(self) -> Data:
192192
When the message is fragmented, :meth:`recv` waits until all fragments
193193
are received, reassembles them, and returns the whole message.
194194
195+
Args:
196+
decode: Set this flag to override the default behavior of returning
197+
:class:`str` or :class:`bytes`. See below for details.
198+
195199
Returns:
196200
A string (:class:`str`) for a Text_ frame or a bytestring
197201
(:class:`bytes`) for a Binary_ frame.
198202
199203
.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
200204
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
201205
206+
You may override this behavior with the ``decode`` argument:
207+
208+
* Set ``decode=False`` to disable UTF-8 decoding of Text_ frames
209+
and return a bytestring (:class:`bytes`). This may be useful to
210+
optimize performance when decoding isn't needed.
211+
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames
212+
and return a string (:class:`str`). This is useful for servers
213+
that send binary frames instead of text frames.
214+
202215
Raises:
203216
ConnectionClosed: When the connection is closed.
204217
RuntimeError: If two coroutines call :meth:`recv` or
205218
:meth:`recv_streaming` concurrently.
206219
207220
"""
208221
try:
209-
return await self.recv_messages.get()
222+
return await self.recv_messages.get(decode)
210223
except EOFError:
211224
raise self.protocol.close_exc from self.recv_exc
212225
except RuntimeError:
@@ -215,7 +228,7 @@ async def recv(self) -> Data:
215228
"is already running recv or recv_streaming"
216229
) from None
217230

218-
async def recv_streaming(self) -> AsyncIterator[Data]:
231+
async def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data]:
219232
"""
220233
Receive the next message frame by frame.
221234
@@ -232,21 +245,34 @@ async def recv_streaming(self) -> AsyncIterator[Data]:
232245
iterator in a partially consumed state, making the connection unusable.
233246
Instead, you should close the connection with :meth:`close`.
234247
248+
Args:
249+
decode: Set this flag to override the default behavior of returning
250+
:class:`str` or :class:`bytes`. See below for details.
251+
235252
Returns:
236253
An iterator of strings (:class:`str`) for a Text_ frame or
237254
bytestrings (:class:`bytes`) for a Binary_ frame.
238255
239256
.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
240257
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
241258
259+
You may override this behavior with the ``decode`` argument:
260+
261+
* Set ``decode=False`` to disable UTF-8 decoding of Text_ frames
262+
and return bytestrings (:class:`bytes`). This may be useful to
263+
optimize performance when decoding isn't needed.
264+
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames
265+
and return strings (:class:`str`). This is useful for servers
266+
that send binary frames instead of text frames.
267+
242268
Raises:
243269
ConnectionClosed: When the connection is closed.
244270
RuntimeError: If two coroutines call :meth:`recv` or
245271
:meth:`recv_streaming` concurrently.
246272
247273
"""
248274
try:
249-
async for frame in self.recv_messages.get_iter():
275+
async for frame in self.recv_messages.get_iter(decode):
250276
yield frame
251277
except EOFError:
252278
raise self.protocol.close_exc from self.recv_exc

src/websockets/asyncio/messages.py

+10
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ async def get(self, decode: bool | None = None) -> Data:
121121
received, then it reassembles the message and returns it. To receive
122122
messages frame by frame, use :meth:`get_iter` instead.
123123
124+
Args:
125+
decode: :obj:`False` disables UTF-8 decoding of text frames and
126+
returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of
127+
binary frames and returns :class:`str`.
128+
124129
Raises:
125130
EOFError: If the stream of frames has ended.
126131
RuntimeError: If two coroutines run :meth:`get` or :meth:`get_iter`
@@ -183,6 +188,11 @@ async def get_iter(self, decode: bool | None = None) -> AsyncIterator[Data]:
183188
This method only makes sense for fragmented messages. If messages aren't
184189
fragmented, use :meth:`get` instead.
185190
191+
Args:
192+
decode: :obj:`False` disables UTF-8 decoding of text frames and
193+
returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of
194+
binary frames and returns :class:`str`.
195+
186196
Raises:
187197
EOFError: If the stream of frames has ended.
188198
RuntimeError: If two coroutines run :meth:`get` or :meth:`get_iter`

tests/asyncio/test_connection.py

+26
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ async def test_recv_binary(self):
167167
await self.remote_connection.send(b"\x01\x02\xfe\xff")
168168
self.assertEqual(await self.connection.recv(), b"\x01\x02\xfe\xff")
169169

170+
async def test_recv_encoded_text(self):
171+
"""recv receives an UTF-8 encoded text message."""
172+
await self.remote_connection.send("😀")
173+
self.assertEqual(await self.connection.recv(decode=False), "😀".encode())
174+
175+
async def test_recv_decoded_binary(self):
176+
"""recv receives an UTF-8 decoded binary message."""
177+
await self.remote_connection.send("😀".encode())
178+
self.assertEqual(await self.connection.recv(decode=True), "😀")
179+
170180
async def test_recv_fragmented_text(self):
171181
"""recv receives a fragmented text message."""
172182
await self.remote_connection.send(["😀", "😀"])
@@ -271,6 +281,22 @@ async def test_recv_streaming_binary(self):
271281
[b"\x01\x02\xfe\xff"],
272282
)
273283

284+
async def test_recv_streaming_encoded_text(self):
285+
"""recv_streaming receives an UTF-8 encoded text message."""
286+
await self.remote_connection.send("😀")
287+
self.assertEqual(
288+
await alist(self.connection.recv_streaming(decode=False)),
289+
["😀".encode()],
290+
)
291+
292+
async def test_recv_streaming_decoded_binary(self):
293+
"""recv_streaming receives a UTF-8 decoded binary message."""
294+
await self.remote_connection.send("😀".encode())
295+
self.assertEqual(
296+
await alist(self.connection.recv_streaming(decode=True)),
297+
["😀"],
298+
)
299+
274300
async def test_recv_streaming_fragmented_text(self):
275301
"""recv_streaming receives a fragmented text message."""
276302
await self.remote_connection.send(["😀", "😀"])

0 commit comments

Comments
 (0)