Skip to content

Commit 3e14821

Browse files
tomchristiepgjones
authored andcommitted
Minimal Headers data structure
1 parent 1540843 commit 3e14821

File tree

5 files changed

+36
-17
lines changed

5 files changed

+36
-17
lines changed

h11/_connection.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def send_failed(self):
534534
def _clean_up_response_headers_for_sending(self, response):
535535
assert type(response) is Response
536536

537-
headers = list(response.headers)
537+
headers = response.headers
538538
need_close = False
539539

540540
# HEAD requests need some special handling: they always act like they
@@ -560,27 +560,27 @@ def _clean_up_response_headers_for_sending(self, response):
560560
# but the HTTP spec says that if our peer does this then we have
561561
# to fix it instead of erroring out, so we'll accord the user the
562562
# same respect).
563-
set_comma_header(headers, b"content-length", [])
563+
headers = set_comma_header(headers, b"content-length", [])
564564
if self.their_http_version is None or self.their_http_version < b"1.1":
565565
# Either we never got a valid request and are sending back an
566566
# error (their_http_version is None), so we assume the worst;
567567
# or else we did get a valid HTTP/1.0 request, so we know that
568568
# they don't understand chunked encoding.
569-
set_comma_header(headers, b"transfer-encoding", [])
569+
headers = set_comma_header(headers, b"transfer-encoding", [])
570570
# This is actually redundant ATM, since currently we
571571
# unconditionally disable keep-alive when talking to HTTP/1.0
572572
# peers. But let's be defensive just in case we add
573573
# Connection: keep-alive support later:
574574
if self._request_method != b"HEAD":
575575
need_close = True
576576
else:
577-
set_comma_header(headers, b"transfer-encoding", ["chunked"])
577+
headers = set_comma_header(headers, b"transfer-encoding", ["chunked"])
578578

579579
if not self._cstate.keep_alive or need_close:
580580
# Make sure Connection: close is set
581581
connection = set(get_comma_header(headers, b"connection"))
582582
connection.discard(b"keep-alive")
583583
connection.add(b"close")
584-
set_comma_header(headers, b"connection", sorted(connection))
584+
headers = set_comma_header(headers, b"connection", sorted(connection))
585585

586586
response.headers = headers

h11/_headers.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@
6262
_field_value_re = re.compile(field_value.encode("ascii"))
6363

6464

65+
class Headers:
66+
__slots__ = ['raw_items']
67+
68+
def __init__(self, raw_items):
69+
self.raw_items = raw_items
70+
71+
def __iter__(self):
72+
for _, name, value in self.raw_items:
73+
yield name, value
74+
75+
def __bool__(self):
76+
return bool(self.raw_items)
77+
78+
def __eq__(self, other):
79+
return list(self) == list(other)
80+
81+
6582
def normalize_and_validate(headers, _parsed=False):
6683
new_headers = []
6784
saw_content_length = False
@@ -75,6 +92,7 @@ def normalize_and_validate(headers, _parsed=False):
7592
value = bytesify(value)
7693
validate(_field_name_re, name, "Illegal header name {!r}", name)
7794
validate(_field_value_re, value, "Illegal header value {!r}", value)
95+
raw_name = name
7896
name = name.lower()
7997
if name == b"content-length":
8098
if saw_content_length:
@@ -99,8 +117,8 @@ def normalize_and_validate(headers, _parsed=False):
99117
error_status_hint=501,
100118
)
101119
saw_transfer_encoding = True
102-
new_headers.append((name, value))
103-
return new_headers
120+
new_headers.append((raw_name, name, value))
121+
return Headers(new_headers)
104122

105123

106124
def get_comma_header(headers, name):
@@ -140,7 +158,7 @@ def get_comma_header(headers, name):
140158
# "100-continue". Splitting on commas is harmless. Case insensitive.
141159
#
142160
out = []
143-
for found_name, found_raw_value in headers:
161+
for _, found_name, found_raw_value in headers.raw_items:
144162
if found_name == name:
145163
found_raw_value = found_raw_value.lower()
146164
for found_split_value in found_raw_value.split(b","):
@@ -153,12 +171,12 @@ def get_comma_header(headers, name):
153171
def set_comma_header(headers, name, new_values):
154172
# The header name `name` is expected to be lower-case bytes.
155173
new_headers = []
156-
for found_name, found_raw_value in headers:
174+
for found_raw_name, found_name, found_raw_value in headers.raw_items:
157175
if found_name != name:
158-
new_headers.append((found_name, found_raw_value))
176+
new_headers.append((found_raw_name, found_raw_value))
159177
for new_value in new_values:
160178
new_headers.append((name, new_value))
161-
headers[:] = normalize_and_validate(new_headers)
179+
return normalize_and_validate(new_headers)
162180

163181

164182
def has_expect_100_continue(request):

h11/_writers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ def write_headers(headers, write):
3838
# "Since the Host field-value is critical information for handling a
3939
# request, a user agent SHOULD generate Host as the first header field
4040
# following the request-line." - RFC 7230
41-
for name, value in headers:
41+
raw_items = headers.raw_items
42+
for raw_name, name, value in raw_items:
4243
if name == b"host":
4344
write(bytesmod(b"%s: %s\r\n", (name, value)))
44-
for name, value in headers:
45+
for raw_name, name, value in raw_items:
4546
if name != b"host":
4647
write(bytesmod(b"%s: %s\r\n", (name, value)))
4748
write(b"\r\n")

h11/tests/test_headers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def test_get_set_comma_header():
8383

8484
assert get_comma_header(headers, b"connection") == [b"close", b"foo", b"bar"]
8585

86-
set_comma_header(headers, b"newthing", ["a", "b"])
86+
headers = set_comma_header(headers, b"newthing", ["a", "b"])
8787

8888
with pytest.raises(LocalProtocolError):
8989
set_comma_header(headers, b"newthing", [" a", "b"])
@@ -96,7 +96,7 @@ def test_get_set_comma_header():
9696
(b"newthing", b"b"),
9797
]
9898

99-
set_comma_header(headers, b"whatever", ["different thing"])
99+
headers = set_comma_header(headers, b"whatever", ["different thing"])
100100

101101
assert headers == [
102102
(b"connection", b"close"),

h11/tests/test_io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from .._events import *
4-
from .._headers import normalize_and_validate
4+
from .._headers import normalize_and_validate, Headers
55
from .._readers import (
66
_obsolete_line_fold,
77
ChunkedReader,
@@ -121,7 +121,7 @@ def test_writers_unusual():
121121
normalize_and_validate([("foo", "bar"), ("baz", "quux")]),
122122
b"foo: bar\r\nbaz: quux\r\n\r\n",
123123
)
124-
tw(write_headers, [], b"\r\n")
124+
tw(write_headers, Headers([]), b"\r\n")
125125

126126
# We understand HTTP/1.0, but we don't speak it
127127
with pytest.raises(LocalProtocolError):

0 commit comments

Comments
 (0)