Skip to content

Commit 8783dcb

Browse files
committed
reserved bit parsing and serializing: add comments, tests, and changelog
1 parent b1d982f commit 8783dcb

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-6
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ dev
77
**API Changes (Backward Compatible)**
88

99
- Setting Identifier are now correctly serialized as 16-bit values, instead of 8-bit.
10+
- GoAwayFrame and WindowUpdateFrame now correctly mask off the reserved bit during
11+
parsing and serialization of stream IDs and window increments, as per RFC 9113,
12+
Sections 6.8 and 6.9.
1013

1114
**API Changes (Backward Incompatible)**
1215

src/hyperframe/frame.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def parse_frame_header(header: memoryview, strict: bool = False) -> tuple[Frame,
132132
length = (fields[0] << 8) + fields[1]
133133
typ_e = fields[2]
134134
flags = fields[3]
135-
stream_id = fields[4] & 0x7FFFFFFF
135+
stream_id = fields[4] & 0x7FFFFFFF # mask off the reserved bit, RFC 9113, Section 4.1
136136

137137
try:
138138
frame = FRAMES[typ_e](stream_id)
@@ -172,7 +172,7 @@ def serialize(self) -> bytes:
172172
self.body_len & 0xFF,
173173
self.type,
174174
flags,
175-
self.stream_id & 0x7FFFFFFF, # Stream ID is 32 bits.
175+
self.stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 4.1
176176
)
177177

178178
return header + body
@@ -271,7 +271,7 @@ def parse_priority_data(self, data: memoryview) -> int:
271271
raise InvalidFrameError(msg) from err
272272

273273
self.exclusive = bool(self.depends_on >> 31)
274-
self.depends_on &= 0x7FFFFFFF
274+
self.depends_on &= 0x7FFFFFFF # mask off the exclusive bit, RFC 9113, Section 6.3
275275
return 5
276276

277277

@@ -623,7 +623,7 @@ def _body_repr(self) -> str:
623623

624624
def serialize_body(self) -> bytes:
625625
data = _STRUCT_LL.pack(
626-
self.last_stream_id & 0x7FFFFFFF,
626+
self.last_stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 6.8
627627
self.error_code,
628628
)
629629
data += self.additional_data
@@ -639,6 +639,7 @@ def parse_body(self, data: memoryview) -> None:
639639
msg = "Invalid GOAWAY body."
640640
raise InvalidFrameError(msg) from err
641641

642+
# mask off the reserved bit, RFC 9113, Section 6.8
642643
self.last_stream_id = self.last_stream_id & 0x7FFFFFFF
643644
self.body_len = len(data)
644645

@@ -678,7 +679,9 @@ def _body_repr(self) -> str:
678679
return f"window_increment={self.window_increment}"
679680

680681
def serialize_body(self) -> bytes:
681-
return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF)
682+
return _STRUCT_L.pack(
683+
self.window_increment & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 6.9
684+
)
682685

683686
def parse_body(self, data: memoryview) -> None:
684687
if len(data) > 4:
@@ -691,6 +694,7 @@ def parse_body(self, data: memoryview) -> None:
691694
msg = "Invalid WINDOW_UPDATE body"
692695
raise InvalidFrameError(msg) from err
693696

697+
# mask off the reserved bit, RFC 9113, Section 6.9
694698
self.window_increment = self.window_increment & 0x7FFFFFFF
695699

696700
if not 1 <= self.window_increment <= 2**31-1:
@@ -910,7 +914,7 @@ def serialize(self) -> bytes:
910914
self.body_len & 0xFF,
911915
self.type,
912916
flags,
913-
self.stream_id & 0x7FFFFFFF, # Stream ID is 32 bits.
917+
self.stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 4.1
914918
)
915919

916920
return header + self.body

tests/test_frames.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,35 @@ def test_short_goaway_frame_errors(self):
670670
with pytest.raises(InvalidFrameError):
671671
decode_frame(s)
672672

673+
def test_goaway_frame_with_reserved_bit_set_parses_properly(self):
674+
s = (
675+
b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
676+
b'\x80\x00\x00\x40' + # Last Stream ID with reserved bit set
677+
b'\x00\x00\x00\x20' + # Error Code
678+
b'hello' # Additional data
679+
)
680+
f = decode_frame(s)
681+
682+
assert isinstance(f, GoAwayFrame)
683+
assert f.flags == set()
684+
assert f.additional_data == b'hello'
685+
assert f.body_len == 13
686+
assert f.last_stream_id == 64
687+
688+
def test_goaway_frame_with_reserved_bit_set_serializes_properly(self):
689+
f = GoAwayFrame()
690+
f.last_stream_id = 64
691+
f.error_code = 32
692+
f.additional_data = b'hello'
693+
694+
s = f.serialize()
695+
assert s == (
696+
b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
697+
b'\x00\x00\x00\x40' + # Last Stream ID
698+
b'\x00\x00\x00\x20' + # Error Code
699+
b'hello' # Additional data
700+
)
701+
673702

674703
class TestWindowUpdateFrame:
675704
def test_repr(self):
@@ -717,6 +746,22 @@ def test_short_windowupdate_frame_errors(self):
717746
with pytest.raises(InvalidDataError):
718747
decode_frame(WindowUpdateFrame(2**31).serialize())
719748

749+
def test_window_update_frame_with_reserved_bit_set_parses_properly(self):
750+
s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x80\x00\x00\x02\x00'
751+
f = decode_frame(s)
752+
753+
assert isinstance(f, WindowUpdateFrame)
754+
assert f.flags == set()
755+
assert f.window_increment == 512
756+
assert f.body_len == 4
757+
758+
def test_window_update_frame_with_reserved_bit_set_serializes_properly(self):
759+
f = WindowUpdateFrame(0)
760+
f.window_increment = 512
761+
762+
s = f.serialize()
763+
assert s == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00'
764+
720765

721766
class TestHeadersFrame:
722767
def test_repr(self):

0 commit comments

Comments
 (0)