Skip to content

Commit 6cea05e

Browse files
committed
Support HTTP response without Content-Length.
Fix #1531.
1 parent 0d2e246 commit 6cea05e

File tree

5 files changed

+85
-6
lines changed

5 files changed

+85
-6
lines changed

src/websockets/asyncio/connection.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1060,13 +1060,22 @@ def eof_received(self) -> None:
10601060
# Feed the end of the data stream to the connection.
10611061
self.protocol.receive_eof()
10621062

1063-
# This isn't expected to generate events.
1064-
assert not self.protocol.events_received()
1063+
# This isn't expected to raise an exception.
1064+
events = self.protocol.events_received()
10651065

10661066
# There is no error handling because send_data() can only write
10671067
# the end of the data stream here and it shouldn't raise errors.
10681068
self.send_data()
10691069

1070+
# This code path is triggered when receiving an HTTP response
1071+
# without a Content-Length header. This is the only case where
1072+
# reading until EOF generates an event; all other events have
1073+
# a known length. Ignore for coverage measurement because tests
1074+
# are in test_client.py rather than test_connection.py.
1075+
for event in events: # pragma: no cover
1076+
# This isn't expected to raise an exception.
1077+
self.process_event(event)
1078+
10701079
# The WebSocket protocol has its own closing handshake: endpoints close
10711080
# the TCP or TLS connection after sending and receiving a close frame.
10721081
# As a consequence, they never need to write after receiving EOF, so

src/websockets/legacy/exceptions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __init__(
5050
headers: datastructures.HeadersLike,
5151
body: bytes = b"",
5252
) -> None:
53-
# If a user passes an int instead of a HTTPStatus, fix it automatically.
53+
# If a user passes an int instead of an HTTPStatus, fix it automatically.
5454
self.status = http.HTTPStatus(status)
5555
self.headers = datastructures.Headers(headers)
5656
self.body = body

src/websockets/sync/connection.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -696,13 +696,22 @@ def recv_events(self) -> None:
696696
# Feed the end of the data stream to the protocol.
697697
self.protocol.receive_eof()
698698

699-
# This isn't expected to generate events.
700-
assert not self.protocol.events_received()
699+
# This isn't expected to raise an exception.
700+
events = self.protocol.events_received()
701701

702702
# There is no error handling because send_data() can only write
703703
# the end of the data stream here and it handles errors itself.
704704
self.send_data()
705705

706+
# This code path is triggered when receiving an HTTP response
707+
# without a Content-Length header. This is the only case where
708+
# reading until EOF generates an event; all other events have
709+
# a known length. Ignore for coverage measurement because tests
710+
# are in test_client.py rather than test_connection.py.
711+
for event in events: # pragma: no cover
712+
# This isn't expected to raise an exception.
713+
self.process_event(event)
714+
706715
except Exception as exc:
707716
# This branch should never run. It's a safety net in case of bugs.
708717
self.logger.error("unexpected internal error", exc_info=True)

tests/asyncio/test_client.py

+30
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,36 @@ def close_connection(self, request):
401401
"connection closed while reading HTTP status line",
402402
)
403403

404+
async def test_http_response(self):
405+
"""Client reads HTTP response."""
406+
407+
def http_response(connection, request):
408+
return connection.respond(http.HTTPStatus.OK, "👌")
409+
410+
async with serve(*args, process_request=http_response) as server:
411+
with self.assertRaises(InvalidStatus) as raised:
412+
async with connect(get_uri(server)):
413+
self.fail("did not raise")
414+
415+
self.assertEqual(raised.exception.response.status_code, 200)
416+
self.assertEqual(raised.exception.response.body.decode(), "👌")
417+
418+
async def test_http_response_without_content_length(self):
419+
"""Client reads HTTP response without a Content-Length header."""
420+
421+
def http_response(connection, request):
422+
response = connection.respond(http.HTTPStatus.OK, "👌")
423+
del response.headers["Content-Length"]
424+
return response
425+
426+
async with serve(*args, process_request=http_response) as server:
427+
with self.assertRaises(InvalidStatus) as raised:
428+
async with connect(get_uri(server)):
429+
self.fail("did not raise")
430+
431+
self.assertEqual(raised.exception.response.status_code, 200)
432+
self.assertEqual(raised.exception.response.body.decode(), "👌")
433+
404434
async def test_junk_handshake(self):
405435
"""Client closes the connection when receiving non-HTTP response from server."""
406436

tests/sync/test_client.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import http
12
import logging
23
import socket
34
import socketserver
@@ -6,7 +7,7 @@
67
import time
78
import unittest
89

9-
from websockets.exceptions import InvalidHandshake, InvalidURI
10+
from websockets.exceptions import InvalidHandshake, InvalidStatus, InvalidURI
1011
from websockets.extensions.permessage_deflate import PerMessageDeflate
1112
from websockets.sync.client import *
1213

@@ -156,6 +157,36 @@ def close_connection(self, request):
156157
"connection closed while reading HTTP status line",
157158
)
158159

160+
def test_http_response(self):
161+
"""Client reads HTTP response."""
162+
163+
def http_response(connection, request):
164+
return connection.respond(http.HTTPStatus.OK, "👌")
165+
166+
with run_server(process_request=http_response) as server:
167+
with self.assertRaises(InvalidStatus) as raised:
168+
with connect(get_uri(server)):
169+
self.fail("did not raise")
170+
171+
self.assertEqual(raised.exception.response.status_code, 200)
172+
self.assertEqual(raised.exception.response.body.decode(), "👌")
173+
174+
def test_http_response_without_content_length(self):
175+
"""Client reads HTTP response without a Content-Length header."""
176+
177+
def http_response(connection, request):
178+
response = connection.respond(http.HTTPStatus.OK, "👌")
179+
del response.headers["Content-Length"]
180+
return response
181+
182+
with run_server(process_request=http_response) as server:
183+
with self.assertRaises(InvalidStatus) as raised:
184+
with connect(get_uri(server)):
185+
self.fail("did not raise")
186+
187+
self.assertEqual(raised.exception.response.status_code, 200)
188+
self.assertEqual(raised.exception.response.body.decode(), "👌")
189+
159190
def test_junk_handshake(self):
160191
"""Client closes the connection when receiving non-HTTP response from server."""
161192

0 commit comments

Comments
 (0)