Skip to content

Commit

Permalink
refactor headers bytes encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
Kriechi committed Dec 18, 2024
1 parent 7fa6a5d commit 5f129a0
Show file tree
Hide file tree
Showing 8 changed files with 35 additions and 28 deletions.
3 changes: 1 addition & 2 deletions src/h2/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .frame_buffer import FrameBuffer
from .settings import Settings, SettingCodes
from .stream import H2Stream, StreamClosedBy
from .utilities import SizeLimitDict, guard_increment_window, utf8_encode_headers
from .utilities import SizeLimitDict, guard_increment_window
from .windows import WindowManager


Expand Down Expand Up @@ -975,7 +975,6 @@ def push_stream(self, stream_id, promised_stream_id, request_headers):
)
self.streams[promised_stream_id] = new_stream

request_headers = utf8_encode_headers(request_headers)
frames = stream.push_stream_in_band(
promised_stream_id, request_headers, self.encoder
)
Expand Down
4 changes: 2 additions & 2 deletions src/h2/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,8 @@ def __init__(self):
def __repr__(self):
return (
"<AlternativeServiceAvailable origin:%s, field_value:%s>" % (
self.origin.decode('utf-8', 'ignore'),
self.field_value.decode('utf-8', 'ignore'),
(self.origin or b'').decode('utf-8', 'ignore'),
(self.field_value or b'').decode('utf-8', 'ignore'),
)
)

Expand Down
4 changes: 4 additions & 0 deletions src/h2/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ def send_headers(self, headers, encoder, end_stream=False):
input_ = StreamInputs.SEND_HEADERS

headers = utf8_encode_headers(headers)

if ((not self.state_machine.client) and
is_informational_response(headers)):
if end_stream:
Expand Down Expand Up @@ -1243,6 +1244,9 @@ def _build_headers_frames(self,
"""
Helper method to build headers or push promise frames.
"""

headers = utf8_encode_headers(headers)

# We need to lowercase the header names, and to ensure that secure
# header fields are kept out of compression contexts.
if self.config.normalize_outbound_headers:
Expand Down
37 changes: 18 additions & 19 deletions src/h2/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,27 +105,26 @@ def extract_method_header(headers):

def is_informational_response(headers):
"""
Searches a header block for a :status header to confirm that a given
Searches headers list for a :status header to confirm that a given
collection of headers are an informational response. Assumes the header
block is well formed: that is, that the HTTP/2 special headers are first
in the block, and so that it can stop looking when it finds the first
header field whose name does not begin with a colon.
are well formed and encoded as bytes: that is, that the HTTP/2 special
headers are first in the block, and so that it can stop looking when it
finds the first header field whose name does not begin with a colon.
:param headers: The HTTP/2 header block.
:param headers: The HTTP/2 headers.
:returns: A boolean indicating if this is an informational response.
"""
for n, v in headers:
# If we find a non-special header, we're done here: stop looping.
if not isinstance(n, bytes) or not isinstance(v, bytes):
raise ProtocolError(f"header not bytes: {n=:r}, {v=:r}") # pragma: no cover

if n and n[0] != SIGIL:
if not n.startswith(b':'):
return False

# This isn't the status header, bail.
if n != b':status':
# If we find a non-special header, we're done here: stop looping.
continue

# If the first digit is a 1, we've got informational headers.
return v[0] == INFORMATIONAL_START
return v.startswith(b'1')


def guard_increment_window(current, increment):
Expand Down Expand Up @@ -515,14 +514,14 @@ def utf8_encode_headers(headers):
tuples that preserve the original type of the header tuple for tuple and
any ``HeaderTuple``.
"""
return [
(
header.__class__(_to_bytes(header[0]), _to_bytes(header[1]))
if isinstance(header, HeaderTuple)
else (_to_bytes(header[0]), _to_bytes(header[1]))
)
for header in headers
]
encoded_headers = []
for header in headers:
h = (_to_bytes(header[0]), _to_bytes(header[1]))
if isinstance(header, HeaderTuple):
encoded_headers.append(header.__class__(h[0], h[1]))
else:
encoded_headers.append(h)
return encoded_headers


def _lowercase_header_names(headers, hdr_validation_flags):
Expand Down
1 change: 1 addition & 0 deletions test/test_h2_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
(b':method', b'GET'),
]


class TestClientUpgrade(object):
"""
Tests of the client-side of the HTTP/2 upgrade dance.
Expand Down
1 change: 1 addition & 0 deletions test/test_head_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
(':method', 'HEAD'),
]


class TestHeadRequest(object):
example_response_headers = [
(b':status', b'200'),
Expand Down
9 changes: 6 additions & 3 deletions test/test_invalid_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,9 @@ def test_headers_event_skipping_validation(self, frame_factory, headers):
c.send_headers(1, headers)

# Ensure headers are still normalized.
headers = h2.utilities.utf8_encode_headers(headers)
norm_headers = h2.utilities.normalize_outbound_headers(
h2.utilities.utf8_encode_headers(headers), None, False
headers, None, False
)
f = frame_factory.build_headers_frame(norm_headers)
assert c.data_to_send() == f.serialize()
Expand All @@ -323,10 +324,12 @@ def test_push_promise_skipping_validation(self, frame_factory, headers):
)
c.receive_data(header_frame.serialize())

# Create push promise frame with normalized headers.
frame_factory.refresh_encoder()

# Create push promise frame with normalized headers.
headers = h2.utilities.utf8_encode_headers(headers)
norm_headers = h2.utilities.normalize_outbound_headers(
h2.utilities.utf8_encode_headers(headers), None, False
headers, None, False
)
pp_frame = frame_factory.build_push_promise_frame(
stream_id=1, promised_stream_id=2, headers=norm_headers
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py39, py310, py311, py312, py13, pypy3, lint, docs, packaging
envlist = py39, py310, py311, py312, py313, pypy3, lint, docs, packaging

[gh-actions]
python =
Expand Down Expand Up @@ -27,7 +27,7 @@ commands = pytest {posargs}

[testenv:lint]
deps =
flake8>=3.9.1,<4
flake8>=7.1.1,<8
commands = flake8 src/ test/

[testenv:docs]
Expand Down

0 comments on commit 5f129a0

Please sign in to comment.