Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Release History

- Require ``event_hint`` when constructing ``RemoteProtocolError``.
This is an API-breaking change.
- Keep the client handshake pending after interim HTTP responses such as
103 Early Hints, rather than reporting them as connection rejections.

1.3.2 (2025-11-20)
------------------
Expand Down
11 changes: 5 additions & 6 deletions src/wsproto/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,11 @@ class RejectConnection(Event):
a response body, the caller should set the ``Content-Length``,
``Content-Type``, and/or ``Transfer-Encoding`` headers as appropriate.

When receiving a ``RejectConnection`` event, the ``has_body`` attribute will
in almost all cases be ``True`` (even if the server set it to ``False``) and
will be followed by at least one ``RejectData`` events, even though the data
itself might be just ``b""``. (The only scenario in which the caller
receives a ``RejectConnection`` with ``has_body == False`` is if the peer
violates sends an informational status code (1xx) other than 101.)
When receiving a ``RejectConnection`` event, the ``has_body`` attribute is
``True`` (even if the server set it to ``False``) and the event is followed
by at least one ``RejectData`` event, even though the data itself might be
just ``b""``. Interim informational responses (1xx other than 101) are
ignored while waiting for the upgrade or a final HTTP response.

The ``has_body`` attribute should only be used when receiving the event. (It
has ) is False the headers must include a
Expand Down
11 changes: 2 additions & 9 deletions src/wsproto/handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,8 @@ def receive_data(self, data: bytes | None) -> None:
if isinstance(event, h11.InformationalResponse):
if event.status_code == 101:
self._events.append(self._establish_client_connection(event))
else:
self._events.append(
RejectConnection(
headers=list(event.headers),
status_code=event.status_code,
has_body=False,
),
)
self._state = ConnectionState.CLOSED
# Other 1xx responses are interim: keep waiting for the
# upgrade or a final HTTP response (RFC 9110, Section 15.2).
elif isinstance(event, h11.Response):
self._state = ConnectionState.REJECTING
self._events.append(
Expand Down
52 changes: 46 additions & 6 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,52 @@ def test_handshake() -> None:
assert events == [AcceptConnection()]


def test_broken_handshake() -> None:
events = _make_handshake(
102, [(b"connection", b"Upgrade"), (b"upgrade", b"websocket")],
)
assert isinstance(events[0], RejectConnection)
assert events[0].status_code == 102
@pytest.mark.parametrize("informational_status", [100, 102, 103, 199])
@pytest.mark.parametrize("final_status", [101, 403])
@pytest.mark.parametrize("coalesced", [False, True])
def test_handshake_informational_responses(
informational_status: int,
final_status: int,
coalesced: bool,
) -> None:
client = WSConnection(CLIENT)
server = h11.Connection(h11.SERVER)
server.receive_data(client.send(Request(host="localhost", target="/")))
request = cast("h11.Request", server.next_event())

# More than one interim response may precede the final response.
interim_data = server.send(h11.InformationalResponse(status_code=informational_status, headers=[]))
interim_data += server.send(h11.InformationalResponse(status_code=103, headers=[]))
if not coalesced:
client.receive_data(interim_data)
assert list(client.events()) == []
assert client.state is ConnectionState.CONNECTING

if final_status == 101:
headers = normed_header_dict(request.headers)
response = h11.InformationalResponse(
status_code=101,
headers=[
(b"connection", b"Upgrade"),
(b"upgrade", b"websocket"),
(b"sec-websocket-accept", generate_accept_token(headers[b"sec-websocket-key"])),
],
)
final_data = server.send(response)
else:
final_data = server.send(h11.Response(status_code=403, headers=[(b"content-length", b"0")]))
final_data += server.send(h11.EndOfMessage())

client.receive_data((interim_data if coalesced else b"") + final_data)
if final_status == 101:
assert list(client.events()) == [AcceptConnection()]
assert client.state is ConnectionState.OPEN
else:
assert list(client.events()) == [
RejectConnection(status_code=403, headers=[(b"content-length", b"0")], has_body=True),
RejectData(data=b"", body_finished=True),
]
assert client.state is ConnectionState.CLOSED


def test_handshake_extra_accept_headers() -> None:
Expand Down