Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit c0b0d26

Browse files
committed
Use HTTPHeaderMap in H2 as well as H1.
1 parent 9e4d8e2 commit c0b0d26

File tree

7 files changed

+104
-237
lines changed

7 files changed

+104
-237
lines changed

hyper/contrib.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def build_response(self, request, resp):
8080
response = Response()
8181

8282
response.status_code = resp.status
83-
response.headers = CaseInsensitiveDict(resp.getheaders())
83+
response.headers = CaseInsensitiveDict(resp.headers.iter_raw())
8484
response.raw = resp
8585
response.reason = resp.reason
8686
response.encoding = get_encoding_from_headers(response.headers)
@@ -136,6 +136,6 @@ def getheaders(self, name):
136136
orig.version = 20
137137
orig.status = resp.status
138138
orig.reason = resp.reason
139-
orig.msg = FakeOriginalResponse(resp.getheaders())
139+
orig.msg = FakeOriginalResponse(resp.headers.iter_raw())
140140

141141
return response

hyper/http20/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ..tls import wrap_socket, H2_NPN_PROTOCOLS
99
from ..common.exceptions import ConnectionResetError
1010
from ..common.bufsocket import BufferedSocket
11+
from ..common.headers import HTTPHeaderMap
1112
from .hpack_compat import Encoder, Decoder
1213
from .stream import Stream
1314
from .frame import (
@@ -191,7 +192,9 @@ def getpushes(self, stream_id=None, capture_all=False):
191192
"""
192193
stream = self._get_stream(stream_id)
193194
for promised_stream_id, headers in stream.getpushes(capture_all):
194-
yield HTTP20Push(headers, self.streams[promised_stream_id])
195+
yield HTTP20Push(
196+
HTTPHeaderMap(headers), self.streams[promised_stream_id]
197+
)
195198

196199
def connect(self):
197200
"""

hyper/http20/response.py

Lines changed: 44 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,22 @@
1010
import zlib
1111

1212
from ..common.decoder import DeflateDecoder
13-
from .util import pop_from_key_value_set
13+
from ..common.headers import HTTPHeaderMap
1414

1515
log = logging.getLogger(__name__)
1616

1717

18-
class Headers(object):
19-
def __init__(self, pairs):
20-
# This conversion to dictionary is unwise, as there may be repeated
21-
# keys, but it's acceptable for an early alpha.
22-
self._headers = dict(pairs)
23-
self._strip_headers()
24-
25-
def getheader(self, name, default=None):
26-
return self._headers.get(name, default)
27-
28-
def getheaders(self):
29-
return list(self._headers.items())
30-
31-
def items(self):
32-
return self._headers.items()
33-
34-
def merge(self, headers):
35-
for n, v in headers:
36-
self._headers[n] = v
37-
self._strip_headers()
38-
39-
def _strip_headers(self):
40-
"""
41-
Strips the headers attached to the instance of any header beginning
42-
with a colon that ``hyper`` doesn't understand. This method logs at
43-
warning level about the deleted headers, for discoverability.
44-
"""
45-
# Convert to list to ensure that we don't mutate the headers while
46-
# we iterate over them.
47-
for name in list(self._headers.keys()):
48-
if name.startswith(':'):
49-
val = self._headers.pop(name)
50-
log.warning("Unknown reserved header: %s: %s", name, val)
18+
def strip_headers(headers):
19+
"""
20+
Strips the headers attached to the instance of any header beginning
21+
with a colon that ``hyper`` doesn't understand. This method logs at
22+
warning level about the deleted headers, for discoverability.
23+
"""
24+
# Convert to list to ensure that we don't mutate the headers while
25+
# we iterate over them.
26+
for name in list(headers.keys()):
27+
if name.startswith(b':'):
28+
del headers[name]
5129

5230

5331
class HTTP20Response(object):
@@ -63,14 +41,15 @@ def __init__(self, headers, stream):
6341
#: HTTP/2, and so is always the empty string.
6442
self.reason = ''
6543

66-
status = pop_from_key_value_set(headers, ':status')[0]
44+
status = headers[b':status'][0]
45+
strip_headers(headers)
6746

6847
#: The status code returned by the server.
6948
self.status = int(status)
7049

71-
# The response headers. These are determined upon creation, assigned
72-
# once, and never assigned again.
73-
self._headers = Headers(headers)
50+
#: The response headers. These are determined upon creation, assigned
51+
#: once, and never assigned again.
52+
self.headers = headers
7453

7554
# The response trailers. These are always intially ``None``.
7655
self._trailers = None
@@ -88,13 +67,29 @@ def __init__(self, headers, stream):
8867
# This 16 + MAX_WBITS nonsense is to force gzip. See this
8968
# Stack Overflow answer for more:
9069
# http://stackoverflow.com/a/2695466/1401686
91-
if self._headers.getheader('content-encoding') == 'gzip':
70+
if b'gzip' in self.headers.get(b'content-encoding', []):
9271
self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
93-
elif self._headers.getheader('content-encoding') == 'deflate':
72+
elif b'deflate' in self.headers.get(b'content-encoding', []):
9473
self._decompressobj = DeflateDecoder()
9574
else:
9675
self._decompressobj = None
9776

77+
@property
78+
def trailers(self):
79+
"""
80+
Trailers on the HTTP message, if any.
81+
82+
.. warning:: Note that this property requires that the stream is
83+
totally exhausted. This means that, if you have not
84+
completely read from the stream, all stream data will be
85+
read into memory.
86+
"""
87+
if self._trailers is None:
88+
self._trailers = self._stream.gettrailers() or HTTPHeaderMap()
89+
strip_headers(self._trailers)
90+
91+
return self._trailers
92+
9893
def read(self, amt=None, decode_content=True):
9994
"""
10095
Reads the response body, or up to the next ``amt`` bytes.
@@ -132,78 +127,14 @@ def read(self, amt=None, decode_content=True):
132127
data += self._decompressobj.flush()
133128

134129
if self._stream.response_headers:
135-
self._headers.merge(self._stream.response_headers)
130+
self.headers.merge(self._stream.response_headers)
136131

137132
# We're at the end. Close the connection.
138133
if not data:
139134
self.close()
140135

141136
return data
142137

143-
def getheader(self, name, default=None):
144-
"""
145-
Return the value of the header ``name``, or ``default`` if there is no
146-
header matching ``name``. If there is more than one header with the
147-
value ``name``, return all of the values joined by ', '. If ``default``
148-
is any iterable other than a single string, its elements are similarly
149-
returned joined by commas.
150-
151-
:param name: The name of the header to get the value of.
152-
:param default: (optional) The return value if the header wasn't sent.
153-
:returns: The value of the header.
154-
"""
155-
return self._headers.getheader(name, default)
156-
157-
def getheaders(self):
158-
"""
159-
Get all the headers sent on the response.
160-
161-
:returns: A list of ``(header, value)`` tuples.
162-
"""
163-
return list(self._headers.items())
164-
165-
def gettrailer(self, name, default=None):
166-
"""
167-
Return the value of the trailer ``name``, or ``default`` if there is no
168-
trailer matching ``name``. If there is more than one trailer with the
169-
value ``name``, return all of the values joined by ', '. If ``default``
170-
is any iterable other than a single string, its elements are similarly
171-
returned joined by commas.
172-
173-
.. warning:: Note that this method requires that the stream is
174-
totally exhausted. This means that, if you have not
175-
completely read from the stream, all stream data will be
176-
read into memory.
177-
178-
:param name: The name of the trailer to get the value of.
179-
:param default: (optional) The return value if the trailer wasn't sent.
180-
:returns: The value of the trailer.
181-
"""
182-
# We need to get the trailers.
183-
if self._trailers is None:
184-
trailers = self._stream.gettrailers() or []
185-
self._trailers = Headers(trailers)
186-
187-
return self._trailers.getheader(name, default)
188-
189-
def gettrailers(self):
190-
"""
191-
Get all the trailers sent on the response.
192-
193-
.. warning:: Note that this method requires that the stream is
194-
totally exhausted. This means that, if you have not
195-
completely read from the stream, all stream data will be
196-
read into memory.
197-
198-
:returns: A list of ``(header, value)`` tuples.
199-
"""
200-
# We need to get the trailers.
201-
if self._trailers is None:
202-
trailers = self._stream.gettrailers() or []
203-
self._trailers = Headers(trailers)
204-
205-
return list(self._trailers.items())
206-
207138
def fileno(self):
208139
"""
209140
Return the ``fileno`` of the underlying socket. This function is
@@ -234,43 +165,21 @@ class HTTP20Push(object):
234165
push mechanism.
235166
"""
236167
def __init__(self, request_headers, stream):
237-
scheme, method, authority, path = (
238-
pop_from_key_value_set(request_headers,
239-
':scheme', ':method', ':authority', ':path')
240-
)
241168
#: The scheme of the simulated request
242-
self.scheme = scheme
169+
self.scheme = request_headers[b':scheme'][0]
243170
#: The method of the simulated request (must be safe and cacheable, e.g. GET)
244-
self.method = method
171+
self.method = request_headers[b':method'][0]
245172
#: The authority of the simulated request (usually host:port)
246-
self.authority = authority
173+
self.authority = request_headers[b':authority'][0]
247174
#: The path of the simulated request
248-
self.path = path
175+
self.path = request_headers[b':path'][0]
249176

250-
self._request_headers = Headers(request_headers)
251-
self._stream = stream
177+
strip_headers(request_headers)
252178

253-
def getrequestheader(self, name, default=None):
254-
"""
255-
Return the value of the simulated request header ``name``, or ``default``
256-
if there is no header matching ``name``. If there is more than one header
257-
with the value ``name``, return all of the values joined by ``', '``.
258-
If ``default`` is any iterable other than a single string, its elements
259-
are similarly returned joined by commas.
260-
261-
:param name: The name of the header to get the value of.
262-
:param default: (optional) The return value if the header wasn't sent.
263-
:returns: The value of the header.
264-
"""
265-
return self._request_headers.getheader(name, default)
266-
267-
def getrequestheaders(self):
268-
"""
269-
Get all the simulated request headers.
179+
#: The headers the server attached to the simulated request.
180+
self.request_headers = request_headers
270181

271-
:returns: A list of ``(header, value)`` tuples.
272-
"""
273-
return self._request_headers.getheaders()
182+
self._stream = stream
274183

275184
def getresponse(self):
276185
"""

hyper/http20/stream.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
Each stream is identified by a monotonically increasing integer, assigned to
1414
the stream by the endpoint that initiated the stream.
1515
"""
16+
from ..common.headers import HTTPHeaderMap
1617
from .exceptions import ProtocolError
1718
from .frame import (
1819
FRAME_MAX_LEN, FRAMES, HeadersFrame, DataFrame, PushPromiseFrame,
1920
WindowUpdateFrame, ContinuationFrame, BlockedFrame
2021
)
21-
from .util import get_from_key_value_set, h2_safe_headers
22+
from .util import h2_safe_headers
2223
import collections
2324
import logging
2425
import zlib
@@ -296,7 +297,7 @@ def getheaders(self):
296297

297298
# Find the Content-Length header if present.
298299
self._in_window_manager.document_size = (
299-
int(get_from_key_value_set(self.response_headers, 'content-length', 0))
300+
int(self.response_headers.get(b'content-length', [0])[0])
300301
)
301302

302303
return self.response_headers
@@ -379,9 +380,9 @@ def _handle_header_block(self, headers):
379380
# The header block may be for trailers or headers. If we've already
380381
# received headers these _must_ be for trailers.
381382
if self.response_headers is None:
382-
self.response_headers = headers
383+
self.response_headers = HTTPHeaderMap(headers)
383384
elif self.response_trailers is None:
384-
self.response_trailers = headers
385+
self.response_trailers = HTTPHeaderMap(headers)
385386
else:
386387
# Received too many headers blocks.
387388
raise ProtocolError("Too many header blocks.")

hyper/http20/util.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,6 @@
88
from collections import defaultdict
99

1010

11-
def get_from_key_value_set(kvset, key, default=None):
12-
"""
13-
Returns a value from a key-value set, or the default if the value isn't
14-
present.
15-
"""
16-
value = pop_from_key_value_set(kvset[:], key)[0]
17-
return value if value is not None else default
18-
19-
def pop_from_key_value_set(kvset, *keys):
20-
"""
21-
Pops the values of ``keys`` from ``kvset`` and returns them as a tuple. If a
22-
key is not found in ``kvset``, ``None`` is used instead.
23-
24-
>>> kvset = [('a',0), ('b',1), ('c',2)]
25-
>>> pop_from_key_value_set(kvset, 'a', 'foo', 'c')
26-
(0, None, 2)
27-
>>> kvset
28-
[('b', 1)]
29-
"""
30-
extracted = [None] * len(keys)
31-
indices_to_remove = []
32-
for index, elem in enumerate(kvset):
33-
key, value = elem
34-
try:
35-
extracted[keys.index(key)] = value
36-
indices_to_remove.append(index)
37-
except ValueError:
38-
pass
39-
40-
for index in indices_to_remove[::-1]:
41-
kvset.pop(index)
42-
43-
return tuple(extracted)
44-
4511
def combine_repeated_headers(kvset):
4612
"""
4713
Given a list of key-value pairs (like for HTTP headers!), combines pairs

0 commit comments

Comments
 (0)