From d9717dc24dd49502efa02fc3cce96982fa047169 Mon Sep 17 00:00:00 2001 From: Vitaliy <160263432+vitalivo@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:15:34 +0300 Subject: [PATCH] Ignore the reserved bit in PUSH_PROMISE identifiers --- CHANGELOG.rst | 3 ++- src/hyperframe/frame.py | 5 ++++- tests/test_reserved_promise.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/test_reserved_promise.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 750102f..f609096 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,7 +19,8 @@ dev **Bugfixes** -- +- Mask the reserved bit in PUSH_PROMISE stream identifiers when parsing and + serializing. 6.1.0 (2025-01-22) ------------------ diff --git a/src/hyperframe/frame.py b/src/hyperframe/frame.py index 5bab673..778dfa7 100644 --- a/src/hyperframe/frame.py +++ b/src/hyperframe/frame.py @@ -515,7 +515,7 @@ def _body_repr(self) -> str: def serialize_body(self) -> bytes: padding_data = self.serialize_padding_data() padding = b"\0" * self.pad_length - data = _STRUCT_L.pack(self.promised_stream_id) + data = _STRUCT_L.pack(self.promised_stream_id & 0x7FFFFFFF) return b"".join([padding_data, data, self.data, padding]) def parse_body(self, data: memoryview) -> None: @@ -529,6 +529,9 @@ def parse_body(self, data: memoryview) -> None: msg = "Invalid PUSH_PROMISE body" raise InvalidFrameError(msg) from err + # Ignore the reserved bit, RFC 9113, Section 6.6. + self.promised_stream_id &= 0x7FFFFFFF + self.data = ( data[padding_data_length + 4:len(data)-self.pad_length].tobytes() ) diff --git a/tests/test_reserved_promise.py b/tests/test_reserved_promise.py new file mode 100644 index 0000000..dcb2eae --- /dev/null +++ b/tests/test_reserved_promise.py @@ -0,0 +1,31 @@ +import struct + +import pytest + +from hyperframe.exceptions import InvalidDataError +from hyperframe.frame import PushPromiseFrame + + +@pytest.mark.parametrize("stream_id", [2, 0x7FFFFFFE]) +@pytest.mark.parametrize("padded", [False, True]) +def test_parse_promised_stream_reserved_bit(stream_id, padded): + frame = PushPromiseFrame(1) + body = struct.pack("!L", stream_id | 0x80000000) + b"headers" + if padded: + frame.flags.add("PADDED") + body = b"\x01" + body + b"\x00" + frame.parse_body(memoryview(body)) + assert frame.promised_stream_id == stream_id + assert frame.data == b"headers" + + +def test_reserved_bit_does_not_make_zero_stream_valid(): + frame = PushPromiseFrame(1) + with pytest.raises(InvalidDataError): + frame.parse_body(memoryview(b"\x80\x00\x00\x00")) + + +@pytest.mark.parametrize("stream_id", [2, 0x7FFFFFFE]) +def test_serialize_promised_stream_reserved_bit(stream_id): + frame = PushPromiseFrame(1, promised_stream_id=stream_id | 0x80000000) + assert frame.serialize_body() == struct.pack("!L", stream_id)