diff --git a/src/h2/connection.py b/src/h2/connection.py index 086112eec..ca2b38329 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -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 @@ -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 ) diff --git a/src/h2/events.py b/src/h2/events.py index 66c3cff4a..2c3409424 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -591,8 +591,8 @@ def __init__(self): def __repr__(self): return ( "" % ( - 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'), ) ) diff --git a/src/h2/stream.py b/src/h2/stream.py index 629bbe548..a6b77289e 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -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: @@ -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: diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 54cd6f210..1d60f1bd2 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -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): @@ -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): diff --git a/test/test_h2_upgrade.py b/test/test_h2_upgrade.py index 1fa851e90..7954a52cb 100644 --- a/test/test_h2_upgrade.py +++ b/test/test_h2_upgrade.py @@ -32,6 +32,7 @@ (b':method', b'GET'), ] + class TestClientUpgrade(object): """ Tests of the client-side of the HTTP/2 upgrade dance. diff --git a/test/test_head_request.py b/test/test_head_request.py index a804f90a7..2f46b72f2 100644 --- a/test/test_head_request.py +++ b/test/test_head_request.py @@ -21,6 +21,7 @@ (':method', 'HEAD'), ] + class TestHeadRequest(object): example_response_headers = [ (b':status', b'200'), diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 996e48aa6..f388cf3fb 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -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() @@ -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 diff --git a/tox.ini b/tox.ini index 74e0c2191..1d3d7fee3 100644 --- a/tox.ini +++ b/tox.ini @@ -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 = @@ -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]