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
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ dev

**Bugfixes**

-
- Omit configured padding from DATA, HEADERS, and PUSH_PROMISE frames when the
PADDED flag is unset.

6.1.0 (2025-01-22)
------------------
Expand Down
9 changes: 6 additions & 3 deletions src/hyperframe/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ def __init__(self, stream_id: int, data: bytes = b"", **kwargs: Any) -> None:

def serialize_body(self) -> bytes:
padding_data = self.serialize_padding_data()
padding = b"\0" * self.pad_length
# 2026-10-11: Padding is payload unless the PADDED flag describes it.
padding = b"\0" * self.pad_length if "PADDED" in self.flags else b""
if isinstance(self.data, memoryview):
self.data = self.data.tobytes()
return b"".join([padding_data, self.data, padding])
Expand Down Expand Up @@ -514,7 +515,8 @@ def _body_repr(self) -> str:

def serialize_body(self) -> bytes:
padding_data = self.serialize_padding_data()
padding = b"\0" * self.pad_length
# 2026-10-11: Padding is payload unless the PADDED flag describes it.
padding = b"\0" * self.pad_length if "PADDED" in self.flags else b""
data = _STRUCT_L.pack(self.promised_stream_id)
return b"".join([padding_data, data, self.data, padding])

Expand Down Expand Up @@ -741,7 +743,8 @@ def _body_repr(self) -> str:

def serialize_body(self) -> bytes:
padding_data = self.serialize_padding_data()
padding = b"\0" * self.pad_length
# 2026-10-11: Padding is payload unless the PADDED flag describes it.
padding = b"\0" * self.pad_length if "PADDED" in self.flags else b""

if "PRIORITY" in self.flags:
priority_data = self.serialize_priority_data()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,37 @@ def test_cannot_parse_invalid_frame_header(self):
with pytest.raises(InvalidFrameError):
Frame.parse_frame_header(b'\x00\x00\x08\x00\x01\x00\x00\x00')

@pytest.mark.parametrize('frame_type', [DataFrame, HeadersFrame, PushPromiseFrame])
@pytest.mark.parametrize('pad_length', [0, 1, 255])
@pytest.mark.parametrize('remove_flag', [False, True])
def test_unpadded_frames_omit_configured_padding(self, frame_type, pad_length, remove_flag):
# 2026-10-11: A cleared PADDED flag must not turn padding into payload.
f = frame_type(1, data=b'hello', pad_length=pad_length)
fixed_length = 0
if frame_type is HeadersFrame:
f.flags.add('PRIORITY')
fixed_length = 5
elif frame_type is PushPromiseFrame:
f.promised_stream_id = 2
fixed_length = 4
if remove_flag:
f.flags.add('PADDED')
assert decode_frame(f.serialize()).data == b'hello'
f.flags.discard('PADDED')

wire = f.serialize()
decoded = decode_frame(wire)
assert decoded.data == b'hello'
assert len(wire) - 9 == fixed_length + len(b'hello')
assert decoded.pad_length == 0
if frame_type is DataFrame:
assert f.flow_controlled_length == len(wire) - 9

f.flags.add('PADDED')
decoded = decode_frame(f.serialize())
assert decoded.data == b'hello'
assert decoded.pad_length == pad_length


class TestDataFrame:
payload = b'\x00\x00\x08\x00\x01\x00\x00\x00\x01testdata'
Expand Down