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: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ dev
**API Changes (Backward Compatible)**

- Support for Python 3.14 has been added.
- ``H2Connection.receive_data`` now accepts any byte-like object that
implements the buffer protocol, such as ``bytes``, ``bytearray``, and
``memoryview``. Existing ``bytes`` callers are unaffected.
- Align CONNECT pseudo-header validation with RFC 9113 s8.3 and RFC 8441 s4.
Ordinary CONNECT now requires ``:method=CONNECT`` and ``:authority``, and
forbids ``:scheme``/``:path``. Extended CONNECT (e.g., WebSocket) requires
Expand Down
6 changes: 5 additions & 1 deletion docs/source/basic-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ socket, in a loop. We then passed that data to the connection object, which
returned us a single event object:
:class:`RemoteSettingsChanged <h2.events.RemoteSettingsChanged>`.

``receive_data`` accepts the ``bytes`` returned by ``recv`` as well as other
byte-like objects that implement the buffer protocol, such as ``bytearray`` and
``memoryview``.

But what we didn't see was anything else. So it seems like all ``curl`` did
was change its settings, but nothing else. If you look at the other ``curl``
window, you'll notice that it hangs for a while and then eventually fails with
Expand Down Expand Up @@ -750,4 +754,4 @@ it, there are a few directions you could investigate:
.. _PyOpenSSL: http://pyopenssl.readthedocs.org/
.. _Eventlet example: https://github.com/python-hyper/h2/blob/master/examples/eventlet/eventlet-server.py
.. _curl: https://curl.se/docs/http2.html
.. _httpx: https://www.python-httpx.org/
.. _httpx: https://www.python-httpx.org/
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ testing = [

linting = [
"ruff>=0.8.0,<1",
"mypy>=1.13.0,<2",
"mypy>=1.16.0,<2",
"typing_extensions>=4.12.2",
]

Expand Down Expand Up @@ -149,6 +149,7 @@ testpaths = [ "tests" ]
[tool.coverage.run]
branch = true
source = [ "h2" ]
omit = [ "*/h2/_typing.py" ]

[tool.coverage.report]
fail_under = 100
Expand Down Expand Up @@ -190,7 +191,7 @@ commands = [
dependency_groups = ["linting"]
commands = [
["ruff", "check", "src/"],
["mypy", "src/"],
["mypy", "--strict-bytes", "src/", "tests/typing/strict_bytes.py"],
]

[tool.tox.env.docs]
Expand Down
21 changes: 21 additions & 0 deletions src/h2/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
h2/_typing
~~~~~~~~~~
Shared typing helpers.
"""
from __future__ import annotations

from typing import Protocol


class Buffer(Protocol):
"""
An object implementing the PEP 688 buffer protocol.
"""

def __buffer__(self, flags: int, /) -> memoryview:
"""
Return a memoryview over this object's bytes.
"""
...
6 changes: 4 additions & 2 deletions src/h2/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@

from hpack.struct import Header, HeaderWeaklyTyped

from ._typing import Buffer


class ConnectionState(Enum):
IDLE = 0
Expand Down Expand Up @@ -1496,12 +1498,12 @@ def _inbound_flow_control_change_from_settings(self, old_value: int | None, new_
for stream in self.streams.values():
stream._inbound_flow_control_change_from_settings(delta)

def receive_data(self, data: bytes) -> list[Event]:
def receive_data(self, data: Buffer) -> list[Event]:
"""
Pass some received HTTP/2 data to the connection for handling.

:param data: The data received from the remote peer on the network.
:type data: ``bytes``
:type data: An object implementing the buffer protocol.
:returns: A list of events that the remote peer triggered by sending
this data.
"""
Expand Down
17 changes: 12 additions & 5 deletions src/h2/frame_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
"""
from __future__ import annotations

from typing import TYPE_CHECKING

from hyperframe.exceptions import InvalidDataError, InvalidFrameError
from hyperframe.frame import ContinuationFrame, Frame, HeadersFrame, PushPromiseFrame

from .exceptions import FrameDataMissingError, FrameTooLargeError, ProtocolError

if TYPE_CHECKING: # pragma: no cover
from ._typing import Buffer

# To avoid a DOS attack based on sending loads of continuation frames, we limit
# the maximum number we're prepared to receive. In this case, we'll set the
# limit to 64, which means the largest encoded header block we can receive by
Expand All @@ -36,25 +41,27 @@ def __init__(self, server: bool = False) -> None:
self._preamble_len = len(self._preamble)
self._headers_buffer: list[HeadersFrame | ContinuationFrame | PushPromiseFrame] = []

def add_data(self, data: bytes) -> None:
def add_data(self, data: Buffer) -> None:
"""
Add more data to the frame buffer.

:param data: A bytestring containing the byte buffer.
"""
data_view = memoryview(data)

if self._preamble_len:
data_len = len(data)
data_len = len(data_view)
of_which_preamble = min(self._preamble_len, data_len)

if self._preamble[:of_which_preamble] != data[:of_which_preamble]:
if self._preamble[:of_which_preamble] != data_view[:of_which_preamble]:
msg = "Invalid HTTP/2 preamble."
raise ProtocolError(msg)

data = data[of_which_preamble:]
data_view = data_view[of_which_preamble:]
self._preamble_len -= of_which_preamble
self._preamble = self._preamble[of_which_preamble:]

self._data += data
self._data += data_view

def _validate_frame_length(self, length: int) -> None:
"""
Expand Down
16 changes: 16 additions & 0 deletions tests/test_basic_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,22 @@ def test_ignores_preamble(self) -> None:
assert not events
assert not c.data_to_send()

@pytest.mark.parametrize("data_wrapper", [bytearray, memoryview])
def test_receive_data_accepts_buffer_types(
self,
data_wrapper,
frame_factory,
) -> None:
"""
``receive_data`` accepts byte-like buffers and handles their contents.
"""
c = h2.connection.H2Connection(config=self.server_config)

events = c.receive_data(data_wrapper(frame_factory.preamble()))

assert not events
assert not c.data_to_send()

@pytest.mark.parametrize("chunk_size", range(1, 24))
def test_drip_feed_preamble(self, chunk_size) -> None:
"""
Expand Down
28 changes: 28 additions & 0 deletions tests/typing/strict_bytes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from hyperframe.frame import Frame
from typing_extensions import assert_type

from h2.connection import H2Connection
from h2.events import Event
from h2.frame_buffer import FrameBuffer


def receive_data_accepts_buffer_types() -> None:
connection = H2Connection()
frame_buffer = FrameBuffer()
bytearray_data = bytearray(b"")
memoryview_data = memoryview(b"")

bytearray_events = connection.receive_data(bytearray_data)
memoryview_events = connection.receive_data(memoryview_data)
frame_buffer.add_data(bytearray_data)
frame_buffer.add_data(memoryview_data)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this test asserting? Please add a validation check after these functions are called with the new argument types.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

buffered_frames = list(frame_buffer)

assert_type(bytearray_events, list[Event])
assert_type(memoryview_events, list[Event])
assert_type(buffered_frames, list[Frame])
assert bytearray_events == []
assert memoryview_events == []
assert buffered_frames == []