Skip to content

Commit beef404

Browse files
authored
Merge pull request #476 from python-hyper/add-extension-frame-support
Add extension frame support.
2 parents 3e3e4f8 + 04cf12d commit beef404

File tree

8 files changed

+73
-10
lines changed

8 files changed

+73
-10
lines changed

HISTORY.rst

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Release History
77
API Changes (Backward-Compatible)
88
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
99

10+
- Added new ``UnknownFrameReceived`` event that fires when unknown extension
11+
frames have been received. This only fires when using hyperframe 5.0 or
12+
later: earlier versions of hyperframe cause us to silently ignore extension
13+
frames.
14+
1015
Bugfixes
1116
~~~~~~~~
1217

docs/source/api.rst

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ Events
7777
.. autoclass:: h2.events.AlternativeServiceAvailable
7878
:members:
7979

80+
.. autoclass:: h2.events.UnknownFrameReceived
81+
:members:
82+
8083

8184
Exceptions
8285
----------

h2/connection.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@
2323
from .events import (
2424
WindowUpdated, RemoteSettingsChanged, PingAcknowledged,
2525
SettingsAcknowledged, ConnectionTerminated, PriorityUpdated,
26-
AlternativeServiceAvailable,
26+
AlternativeServiceAvailable, UnknownFrameReceived
2727
)
2828
from .exceptions import (
2929
ProtocolError, NoSuchStreamError, FlowControlError, FrameTooLargeError,
3030
TooManyStreamsError, StreamClosedError, StreamIDTooLowError,
31-
NoAvailableStreamIDError, UnsupportedFrameError, RFC1122Error,
32-
DenialOfServiceError
31+
NoAvailableStreamIDError, RFC1122Error, DenialOfServiceError
3332
)
3433
from .frame_buffer import FrameBuffer
3534
from .settings import Settings, SettingCodes
@@ -46,6 +45,15 @@ class OversizedHeaderListError(Exception):
4645
pass
4746

4847

48+
try:
49+
from hyperframe.frame import ExtensionFrame
50+
except ImportError: # Platform-specific: Hyperframe < 5.0.0
51+
# If the frame doesn't exist, that's just fine: we'll define it ourselves
52+
# and the method will just never be called.
53+
class ExtensionFrame(object):
54+
pass
55+
56+
4957
class ConnectionState(Enum):
5058
IDLE = 0
5159
CLIENT_OPEN = 1
@@ -404,6 +412,7 @@ def __init__(self, client_side=True, header_encoding='utf-8', config=None):
404412
GoAwayFrame: self._receive_goaway_frame,
405413
ContinuationFrame: self._receive_naked_continuation,
406414
AltSvcFrame: self._receive_alt_svc_frame,
415+
ExtensionFrame: self._receive_unknown_frame
407416
}
408417

409418
def _prepare_for_sending(self, frames):
@@ -1575,10 +1584,6 @@ def _receive_frame(self, frame):
15751584
# Closed implicitly, also a connection error, but of type
15761585
# PROTOCOL_ERROR.
15771586
raise
1578-
except KeyError as e: # pragma: no cover
1579-
# We don't have a function for handling this frame. Let's call this
1580-
# a PROTOCOL_ERROR and exit.
1581-
raise UnsupportedFrameError("Unexpected frame: %s" % frame)
15821587
else:
15831588
self._prepare_for_sending(frames)
15841589

@@ -1922,6 +1927,23 @@ def _receive_alt_svc_frame(self, frame):
19221927

19231928
return frames, events
19241929

1930+
def _receive_unknown_frame(self, frame):
1931+
"""
1932+
We have received a frame that we do not understand. This is almost
1933+
certainly an extension frame, though it's impossible to be entirely
1934+
sure.
1935+
1936+
RFC 7540 § 5.5 says that we MUST ignore unknown frame types: so we
1937+
do. We do notify the user that we received one, however.
1938+
"""
1939+
# All we do here is log.
1940+
self.config.logger.debug(
1941+
"Received unknown extension frame (ID %d)", frame.stream_id
1942+
)
1943+
event = UnknownFrameReceived()
1944+
event.frame = frame
1945+
return [], [event]
1946+
19251947
def _local_settings_acked(self):
19261948
"""
19271949
Handle the local settings being ACKed, update internal state.

h2/events.py

+22
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,28 @@ def __repr__(self):
574574
)
575575

576576

577+
class UnknownFrameReceived(Event):
578+
"""
579+
The UnknownFrameReceived event is fired when the remote peer sends a frame
580+
that hyper-h2 does not understand. This occurs primarily when the remote
581+
peer is employing HTTP/2 extensions that hyper-h2 doesn't know anything
582+
about.
583+
584+
RFC 7540 requires that HTTP/2 implementations ignore these frames. hyper-h2
585+
does so. However, this event is fired to allow implementations to perform
586+
special processing on those frames if needed (e.g. if the implementation
587+
is capable of handling the frame itself).
588+
589+
.. versionadded:: 2.7.0
590+
"""
591+
def __init__(self):
592+
#: The hyperframe Frame object that encapsulates the received frame.
593+
self.frame = None
594+
595+
def __repr__(self):
596+
return "<UnknownFrameReceived>"
597+
598+
577599
def _bytes_representation(data):
578600
"""
579601
Converts a bytestring into something that is safe to print on all Python

h2/frame_buffer.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ def _parse_frame_header(self, data):
6565
"""
6666
try:
6767
frame, length = Frame.parse_frame_header(data[:9])
68-
except UnknownFrameError as e:
68+
except UnknownFrameError as e: # Platform-specific: Hyperframe < 5.0
6969
# Here we do something a bit odd. We want to consume the frame data
7070
# as consistently as possible, but we also don't ever want to yield
7171
# None. Instead, we make sure that, if there is no frame, we
7272
# recurse into ourselves.
73+
# This can only happen now on older versions of hyperframe.
74+
# TODO: Remove in 3.0
7375
length = e.length
7476
frame = None
7577
except ValueError as e:

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
'Programming Language :: Python :: Implementation :: PyPy',
6363
],
6464
install_requires=[
65-
'hyperframe>=3.1, <5, !=4.0.0',
65+
'hyperframe>=3.1, <6, !=4.0.0',
6666
'hpack>=2.2, <3',
6767
],
6868
extras_require={

test/test_basic_logic.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1574,8 +1574,10 @@ def test_unknown_frames_are_ignored(self, frame_factory, frame_id):
15741574
f.type = frame_id
15751575

15761576
events = c.receive_data(f.serialize())
1577-
assert not events
15781577
assert not c.data_to_send()
1578+
assert len(events) == 1
1579+
assert isinstance(events[0], h2.events.UnknownFrameReceived)
1580+
assert isinstance(events[0].frame, hyperframe.frame.ExtensionFrame)
15791581

15801582
def test_can_send_goaway_repeatedly(self, frame_factory):
15811583
"""

test/test_events.py

+7
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ def test_alternativeserviceavailable_repr(self):
323323
'field_value:h2=":8000"; ma=60>'
324324
)
325325

326+
def test_unknownframereceived_repr(self):
327+
"""
328+
UnknownFrameReceived has a useful debug representation.
329+
"""
330+
e = h2.events.UnknownFrameReceived()
331+
assert repr(e) == '<UnknownFrameReceived>'
332+
326333

327334
def all_events():
328335
"""

0 commit comments

Comments
 (0)