Skip to content

Commit f717395

Browse files
committed
Merge branch 'add-unknown-frame' of https://github.com/haikuginger/hyperframe into haikuginger-add-unknown-frame
2 parents 4247785 + 32d01bf commit f717395

File tree

3 files changed

+102
-6
lines changed

3 files changed

+102
-6
lines changed

docs/source/api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ you need is not present!
6363
:show-inheritance:
6464
:members:
6565

66+
.. autoclass:: hyperframe.frame.ExtensionFrame
67+
:show-inheritance:
68+
:members:
69+
6670
.. autodata:: hyperframe.frame.FRAMES
6771

6872
Exceptions

hyperframe/frame.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,21 @@ def __repr__(self):
8888
)
8989

9090
@staticmethod
91-
def parse_frame_header(header):
91+
def parse_frame_header(header, strict=False):
9292
"""
9393
Takes a 9-byte frame header and returns a tuple of the appropriate
9494
Frame object and the length that needs to be read from the socket.
9595
9696
This populates the flags field, and determines how long the body is.
9797
98+
:param strict: Whether to raise an exception when encountering a frame
99+
not defined by spec and implemented by hyperframe.
100+
98101
:raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown
99102
type is received.
103+
104+
.. versionchanged:: 5.0.0
105+
Added :param:`strict` to accommodate :class:`ExtensionFrame`
100106
"""
101107
try:
102108
fields = _STRUCT_HBBBL.unpack(header)
@@ -110,7 +116,15 @@ def parse_frame_header(header):
110116
stream_id = fields[4] & 0x7FFFFFFF
111117

112118
if type not in FRAMES:
113-
raise UnknownFrameError(type, length)
119+
if strict:
120+
raise UnknownFrameError(type, length)
121+
122+
frame = ExtensionFrame(
123+
type=type,
124+
flag_byte=flags,
125+
stream_id=stream_id,
126+
)
127+
return (frame, length)
114128

115129
frame = FRAMES[type](stream_id)
116130
frame.parse_flags(flags)
@@ -736,6 +750,46 @@ def parse_body(self, data):
736750
self.body_len = len(data)
737751

738752

753+
class ExtensionFrame(Frame):
754+
"""
755+
ExtensionFrame is used to wrap frames which are not natively interpretable
756+
by hyperframe.
757+
758+
Although certain byte prefixes are ordained by specification to have
759+
certain contextual meanings, frames with other prefixes are not prohibited,
760+
and may be used to communicate arbitrary meaning between HTTP/2 peers.
761+
762+
Thus, hyperframe, rather than raising an exception when such a frame is
763+
encountered, wraps it in a generic frame to be properly acted upon by
764+
upstream consumers which might have additional context on how to use it.
765+
766+
.. versionadded:: 5.0.0
767+
"""
768+
769+
stream_association = _STREAM_ASSOC_EITHER
770+
771+
def __init__(self, type, flag_byte, stream_id, **kwargs):
772+
super(ExtensionFrame, self).__init__(stream_id, **kwargs)
773+
self.type = type
774+
self.flag_byte = flag_byte
775+
776+
def assign_flag_mapping(self, flags):
777+
"""
778+
When initially created, an ExtensionFrame has no concept of what flags
779+
might be relevant to it, since a frame's flags are defined in the
780+
specification for that frame. This method allows a frame to be assigned
781+
a flag mapping after the fact, allowing downstream consumers to modify
782+
ExtensionFrame to fit their needs.
783+
"""
784+
self.defined_flags = flags
785+
self.flags = Flags(self.defined_flags)
786+
return self.parse_flags(self.flag_byte)
787+
788+
def parse_body(self, data):
789+
self.body = data.tobytes()
790+
self.body_len = len(data)
791+
792+
739793
_FRAME_CLASSES = [
740794
DataFrame,
741795
HeadersFrame,

test/test_frames.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# -*- coding: utf-8 -*-
2+
from hyperframe.flags import Flag
23
from hyperframe.frame import (
34
Frame, Flags, DataFrame, PriorityFrame, RstStreamFrame, SettingsFrame,
45
PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame, HeadersFrame,
5-
ContinuationFrame, AltSvcFrame
6+
ContinuationFrame, AltSvcFrame, ExtensionFrame
67
)
78
from hyperframe.exceptions import (
89
UnknownFrameError, InvalidPaddingError, InvalidFrameError
@@ -35,10 +36,12 @@ def test_base_frame_cant_parse_body(self):
3536
with pytest.raises(NotImplementedError):
3637
f.parse_body(data)
3738

38-
def test_parse_frame_header_unknown_type(self):
39+
def test_parse_frame_header_unknown_type_strict(self):
3940
with pytest.raises(UnknownFrameError) as excinfo:
40-
Frame.parse_frame_header(b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01')
41-
41+
Frame.parse_frame_header(
42+
b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01',
43+
strict=True
44+
)
4245
exception = excinfo.value
4346
assert exception.frame_type == 0xFF
4447
assert exception.length == 0x59
@@ -53,6 +56,41 @@ def test_parse_frame_header_ignore_first_bit_of_stream_id(self):
5356

5457
assert f.stream_id == 0
5558

59+
def test_parse_frame_header_unknown_type(self):
60+
f, l = Frame.parse_frame_header(
61+
b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01'
62+
)
63+
assert f.type == 0xFF
64+
assert l == 0x59
65+
assert isinstance(f, ExtensionFrame)
66+
assert f.stream_id == 1
67+
68+
def test_add_flag_options_later_unknown_type(self):
69+
f, l = Frame.parse_frame_header(
70+
b'\x00\x00\x59\xFF\x09\x00\x00\x00\x01'
71+
)
72+
assert f.type == 0xFF
73+
assert l == 0x59
74+
new_flags = [
75+
Flag('FANCY_FLAG', 0x01),
76+
Flag('REAL_THING', 0x04),
77+
Flag('HUUUUUUGE', 0x08),
78+
]
79+
flags = f.assign_flag_mapping(new_flags)
80+
assert f.defined_flags == new_flags
81+
assert flags
82+
assert 'FANCY_FLAG' in flags
83+
assert 'REAL_THING' not in flags
84+
assert 'HUUUUUUGE' in flags
85+
86+
def test_parse_body_unknown_type(self):
87+
f = decode_frame(
88+
b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!'
89+
)
90+
assert f.body == b'hello world!'
91+
assert f.body_len == 12
92+
assert f.stream_id == 1
93+
5694
def test_repr(self, monkeypatch):
5795
f = Frame(stream_id=0)
5896
monkeypatch.setattr(Frame, "serialize_body", lambda _: b"body")

0 commit comments

Comments
 (0)