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

Commit c862c04

Browse files
committed
Merge remote-tracking branch 'upstream/development' into development
2 parents e32b5be + 2494811 commit c862c04

File tree

10 files changed

+95
-44
lines changed

10 files changed

+95
-44
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ script:
3131
if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then
3232
py.test test/
3333
else
34-
py.test -n 4 --cov hyper test/
34+
py.test -n 1 --cov hyper test/
3535
coverage report -m --fail-under 100
3636
fi
3737
fi

HISTORY.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Release History
22
===============
33

4+
dev
5+
---
6+
7+
*Bugfixes*
8+
9+
- Overriding HTTP/2 special headers no longer leads to ill-formed header blocks
10+
with special headers at the end.
11+
412
0.5.0 (2015-10-11)
513
------------------
614

hyper/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,14 @@ def set_request_data(args):
164164
body, headers, params = {}, {}, {}
165165
for i in args.items:
166166
if i.sep == SEP_HEADERS:
167-
headers[i.key] = i.value
167+
if i.key:
168+
headers[i.key] = i.value
169+
else:
170+
# when overriding a HTTP/2 special header there will be a leading
171+
# colon, which tricks the command line parser into thinking
172+
# the header is empty
173+
k, v = i.value.split(':', 1)
174+
headers[':' + k] = v
168175
elif i.sep == SEP_QUERY:
169176
params[i.key] = i.value
170177
elif i.sep == SEP_DATA:

hyper/common/connection.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class HTTPConnection(object):
2525
hostname, and optionally may include a port: for example,
2626
``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``.
2727
:param port: (optional) The port to connect to. If not provided and one also
28-
isn't provided in the ``host`` parameter, defaults to 443.
28+
isn't provided in the ``host`` parameter, defaults to 80.
2929
:param secure: (optional) Whether the request should use TLS.
3030
Defaults to ``False`` for most requests, but to ``True`` for any
3131
request issued to port 443.
@@ -42,7 +42,7 @@ class HTTPConnection(object):
4242
If not provided then hyper's default ``SSLContext`` is used instead.
4343
:param proxy_host: (optional) The proxy to connect to. This can be an IP address
4444
or a host name and may include a port.
45-
:param proxy_port: (optional) The proxy port to connect to. If not provided
45+
:param proxy_port: (optional) The proxy port to connect to. If not provided
4646
and one also isn't provided in the ``proxy`` parameter, defaults to 8080.
4747
"""
4848
def __init__(self,
@@ -59,12 +59,12 @@ def __init__(self,
5959
self._host = host
6060
self._port = port
6161
self._h1_kwargs = {
62-
'secure': secure, 'ssl_context': ssl_context,
63-
'proxy_host': proxy_host, 'proxy_port': proxy_port
62+
'secure': secure, 'ssl_context': ssl_context,
63+
'proxy_host': proxy_host, 'proxy_port': proxy_port
6464
}
6565
self._h2_kwargs = {
6666
'window_manager': window_manager, 'enable_push': enable_push,
67-
'secure': secure, 'ssl_context': ssl_context,
67+
'secure': secure, 'ssl_context': ssl_context,
6868
'proxy_host': proxy_host, 'proxy_port': proxy_port
6969
}
7070

hyper/common/headers.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,28 @@ def iter_raw(self):
184184
def replace(self, key, value):
185185
"""
186186
Replace existing header with new value. If header doesn't exist this
187-
method work like ``__setitem__``. Replacing leads to deletion of all
188-
exsiting headers with the same name.
187+
method work like ``__setitem__``. Replacing leads to deletion of all
188+
existing headers with the same name.
189189
"""
190-
try:
191-
del self[key]
192-
except KeyError:
193-
pass
190+
key = to_bytestring(key)
191+
indices = []
192+
for (i, (k, v)) in enumerate(self._items):
193+
if _keys_equal(k, key):
194+
indices.append(i)
195+
196+
# If the key isn't present, this is easy: just append and abort early.
197+
if not indices:
198+
self._items.append((key, value))
199+
return
200+
201+
# Delete all but the first. I swear, this is the correct slicing
202+
# syntax!
203+
base_index = indices[0]
204+
for i in indices[:0:-1]:
205+
self._items.pop(i)
194206

195-
self[key] = value
207+
del self._items[base_index]
208+
self._items.insert(base_index, (key, value))
196209

197210
def merge(self, other):
198211
"""

hyper/http20/connection.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ..common.exceptions import ConnectionResetError
1010
from ..common.bufsocket import BufferedSocket
1111
from ..common.headers import HTTPHeaderMap
12-
from ..common.util import to_host_port_tuple, to_native_string
12+
from ..common.util import to_host_port_tuple, to_native_string, to_bytestring
1313
from ..packages.hyperframe.frame import (
1414
FRAMES, DataFrame, HeadersFrame, PushPromiseFrame, RstStreamFrame,
1515
SettingsFrame, Frame, WindowUpdateFrame, GoAwayFrame, PingFrame,
@@ -179,8 +179,8 @@ def request(self, method, url, body=None, headers={}):
179179
self.putheader(name, value, stream_id, replace=is_default)
180180

181181
# Convert the body to bytes if needed.
182-
if isinstance(body, str):
183-
body = body.encode('utf-8')
182+
if body:
183+
body = to_bytestring(body)
184184

185185
self.endheaders(message_body=body, final=True, stream_id=stream_id)
186186

test/test_cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,20 @@ def test_cli_with_system_exit(argv):
110110
{'method': 'GET', 'url.path': '/?param=test'}),
111111
(['POST', 'example.com', 'data=test'],
112112
{'method': 'POST', 'body': '{"data": "test"}'}),
113+
(['GET', 'example.com', ':authority:example.org'],
114+
{'method': 'GET', 'headers': {
115+
':authority': 'example.org'}}),
116+
(['GET', 'example.com', ':authority:example.org', 'x-test:header'],
117+
{'method': 'GET', 'headers': {
118+
':authority': 'example.org',
119+
'x-test':'header'}}),
113120
], ids=[
114121
'specified "--debug" option',
115122
'specified host and additional header',
116123
'specified host and get parameter',
117124
'specified host and post data',
125+
'specified host and override default header',
126+
'specified host and override default header and additional header',
118127
])
119128
def test_parse_argument(argv, expected):
120129
args = parse_argument(argv)

test/test_headers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ def test_replacing(self):
272272

273273
assert list(h.items()) == [
274274
(b'name', b'value'),
275-
(b'name3', b'value3'),
276275
(b'name2', b'42'),
276+
(b'name3', b'value3'),
277277
(b'name4', b'other_value'),
278278
]
279279

test/test_hyper.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def test_putheader_replaces_headers(self):
122122
assert list(s.headers.items()) == [
123123
(b':method', b'GET'),
124124
(b':scheme', b'https'),
125-
(b':path', b'/'),
126125
(b':authority', b'www.example.org'),
126+
(b':path', b'/'),
127127
(b'name', b'value2'),
128128
]
129129

@@ -221,6 +221,22 @@ def test_putrequest_sends_data(self):
221221
assert len(sock.queue) == 2
222222
assert c._out_flow_control_window == 65535 - len(b'hello')
223223

224+
def test_request_with_utf8_bytes_body(self):
225+
c = HTTP20Connection('www.google.com')
226+
c._sock = DummySocket()
227+
body = '你好' if is_py2 else '你好'.encode('utf-8')
228+
c.request('GET', '/', body=body)
229+
230+
assert c._out_flow_control_window == 65535 - len(body)
231+
232+
def test_request_with_unicode_body(self):
233+
c = HTTP20Connection('www.google.com')
234+
c._sock = DummySocket()
235+
body = '你好'.decode('unicode-escape') if is_py2 else '你好'
236+
c.request('GET', '/', body=body)
237+
238+
assert c._out_flow_control_window == 65535 - len(body.encode('utf-8'))
239+
224240
def test_different_request_headers(self):
225241
sock = DummySocket()
226242

@@ -581,7 +597,7 @@ def test_recv_cb_n_times(self):
581597

582598
def consume_single_frame():
583599
mutable['counter'] += 1
584-
600+
585601
c._consume_single_frame = consume_single_frame
586602
c._recv_cb()
587603

@@ -779,7 +795,7 @@ def test_streams_can_replace_none_headers(self):
779795
(b"name", b"value"),
780796
(b"other_name", b"other_value")
781797
]
782-
798+
783799
def test_stream_opening_sends_headers(self):
784800
def data_callback(frame):
785801
assert isinstance(frame, HeadersFrame)
@@ -1548,7 +1564,7 @@ def test_connection_error_when_send_out_of_range_frame(self):
15481564
class NullEncoder(object):
15491565
@staticmethod
15501566
def encode(headers):
1551-
1567+
15521568
def to_str(v):
15531569
if is_py2:
15541570
return str(v)
@@ -1557,7 +1573,7 @@ def to_str(v):
15571573
v = str(v, 'utf-8')
15581574
return v
15591575

1560-
return '\n'.join("%s%s" % (to_str(name), to_str(val))
1576+
return '\n'.join("%s%s" % (to_str(name), to_str(val))
15611577
for name, val in headers)
15621578

15631579

0 commit comments

Comments
 (0)