From f5a0d3f4a8b44d24ea35d5219f9dfce0d59e931f Mon Sep 17 00:00:00 2001 From: adarshx01 Date: Mon, 17 Aug 2026 18:30:43 +0530 Subject: [PATCH] Ignore leftover frames received after the connection is closed. --- CHANGELOG.rst | 8 ++++++++ src/h2/connection.py | 7 +++++++ tests/test_basic_logic.py | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 44134f898..3afe6f3b4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Release History =============== +4.4.2 (unreleased) +------------------ + +**Bugfixes** + +- Ignore leftover frames received after the connection has closed, instead of + raising ``ProtocolError``. Further GOAWAY frames are still processed. + 4.4.1 (2026-08-03) ------------------ diff --git a/src/h2/connection.py b/src/h2/connection.py index e5059a21c..6db29d705 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1541,6 +1541,13 @@ def _receive_frame(self, frame: Frame) -> list[Event]: """ events: list[Event] self.config.logger.trace("Received frame: %s", repr(frame)) + # Leftover frames can still be in the TCP buffer after we closed. + # Extra GOAWAYs are already valid; anything else is not a protocol error. + if ( + self.state_machine.state == ConnectionState.CLOSED + and not isinstance(frame, GoAwayFrame) + ): + return [] try: # I don't love using __class__ here, maybe reconsider it. frames, events = self._frame_dispatch_table[frame.__class__](frame) diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index d1bc0f1eb..d286aaab5 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -1788,6 +1788,31 @@ def test_receiving_goaway_frame(self, frame_factory) -> None: assert not c.data_to_send() + def test_frames_after_goaway_are_ignored(self, frame_factory) -> None: + """ + Frames already in the receive buffer after GOAWAY must not raise + ProtocolError. Further GOAWAY frames are still accepted. + """ + c = h2.connection.H2Connection(config=self.server_config) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + c.clear_outbound_data_buffer() + + goaway = frame_factory.build_goaway_frame(last_stream_id=0) + events = c.receive_data(goaway.serialize()) + assert len(events) == 1 + assert isinstance(events[0], h2.events.ConnectionTerminated) + + ping = frame_factory.build_ping_frame(b"\x01\x02\x03\x04\x05\x06\x07\x08") + events = c.receive_data(ping.serialize()) + assert events == [] + assert not c.data_to_send() + assert c.state_machine.state == h2.connection.ConnectionState.CLOSED + + events = c.receive_data(goaway.serialize()) + assert len(events) == 1 + assert isinstance(events[0], h2.events.ConnectionTerminated) + def test_receiving_multiple_goaway_frames(self, frame_factory) -> None: """ Multiple GOAWAY frames can be received at once, and are allowed. Each