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

Commit 25cdd6e

Browse files
committed
Merge pull request #116 from Lukasa/abstraction
HTTP/1.1 and HTTP/2 abstraction layer
2 parents 853a6d7 + 6b97aca commit 25cdd6e

File tree

12 files changed

+284
-93
lines changed

12 files changed

+284
-93
lines changed

README.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ improved speed, lower bandwidth usage, better connection management, and more.
1313

1414
``hyper`` provides these benefits to your Python code. How? Like this::
1515

16-
from hyper import HTTP20Connection
16+
from hyper import HTTPConnection
1717

18-
conn = HTTP20Connection('http2bin.org:443')
18+
conn = HTTPConnection('http2bin.org:443')
1919
conn.request('GET', '/get')
20-
resp = conn.getresponse()
20+
resp = conn.get_response()
2121

2222
print(resp.read())
2323

docs/source/advanced.rst

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ may want to keep your connections alive only as long as you know you'll need
1313
them. In HTTP/2 this is generally not something you should do unless you're
1414
very confident you won't need the connection again anytime soon. However, if
1515
you decide you want to avoid keeping the connection open, you can use the
16-
:class:`HTTP20Connection <hyper.HTTP20Connection>` and
17-
:class:`HTTP11Connection <hyper.HTTP11Connection>` as context managers::
16+
:class:`HTTPConnection <hyper.HTTPConnection>` as a context manager::
1817

19-
with HTTP20Connection('http2bin.org') as conn:
18+
with HTTPConnection('http2bin.org') as conn:
2019
conn.request('GET', '/get')
21-
data = conn.getresponse().read()
20+
data = conn.get_response().read()
2221

2322
analyse(data)
2423

@@ -57,10 +56,9 @@ Very easy!
5756
Multithreading
5857
--------------
5958

60-
Currently, ``hyper``'s :class:`HTTP20Connection <hyper.HTTP20Connection>` and
61-
:class:`HTTP11Connection <hyper.HTTP11Connection>` classes are **not**
62-
thread-safe. Thread-safety is planned for ``hyper``'s core objects, but in this
63-
early alpha it is not a high priority.
59+
Currently, ``hyper``'s :class:`HTTPConnection <hyper.HTTPConnection>` class
60+
is **not** thread-safe. Thread-safety is planned for ``hyper``'s core objects,
61+
but in this early alpha it is not a high priority.
6462

6563
To use ``hyper`` in a multithreaded context the recommended thing to do is to
6664
place each connection in its own thread. Each thread should then have a request
@@ -155,8 +153,8 @@ headers for the original request, after, or in the middle of sending the
155153
response body.
156154

157155
In order to receive pushed resources, the
158-
:class:`HTTP20Connection <hyper.HTTP20Connection>` object must be constructed
159-
with ``enable_push=True``.
156+
:class:`HTTPConnection <hyper.HTTPConnection>` object must be constructed with
157+
``enable_push=True``.
160158

161159
You may retrieve the push promises that the server has sent *so far* by calling
162160
:meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`, which returns a

docs/source/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ improved speed, lower bandwidth usage, better connection management, and more.
1414

1515
``hyper`` provides these benefits to your Python code. How? Like this::
1616

17-
from hyper import HTTP20Connection
17+
from hyper import HTTPConnection
1818

19-
conn = HTTP20Connection('http2bin.org:443')
19+
conn = HTTPConnection('http2bin.org:443')
2020
conn.request('GET', '/get')
2121
resp = conn.get_response()
2222

docs/source/quickstart.rst

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,20 @@ http2bin.org, a HTTP/1.1 and HTTP/2 testing service.
5959

6060
Begin by getting the homepage::
6161

62-
>>> from hyper import HTTP20Connection
63-
>>> c = HTTP20Connection('http2bin.org')
62+
>>> from hyper import HTTPConnection
63+
>>> c = HTTPConnection('http2bin.org')
6464
>>> c.request('GET', '/')
6565
1
6666
>>> resp = c.get_response()
6767

6868
Used in this way, ``hyper`` behaves exactly like ``http.client``. You can make
6969
sequential requests using the exact same API you're accustomed to. The only
7070
difference is that
71-
:meth:`HTTP20Connection.request() <hyper.HTTP20Connection.request>` returns a
72-
value, unlike the equivalent ``http.client`` function. The return value is the
73-
HTTP/2 *stream identifier*. If you're planning to use ``hyper`` in this very
74-
simple way, you can choose to ignore it, but it's potentially useful. We'll
75-
come back to it.
71+
:meth:`HTTPConnection.request() <hyper.HTTPConnection.request>` may return a
72+
value, unlike the equivalent ``http.client`` function. If present, the return
73+
value is the HTTP/2 *stream identifier*. If you're planning to use ``hyper``
74+
in this very simple way, you can choose to ignore it, but it's potentially
75+
useful. We'll come back to it.
7676

7777
Once you've got the data, things diverge a little bit::
7878

@@ -101,8 +101,8 @@ the response from any of them, and switch between them using their stream IDs.
101101

102102
For example::
103103

104-
>>> from hyper import HTTP20Connection
105-
>>> c = HTTP20Connection('http2bin.org')
104+
>>> from hyper import HTTPConnection
105+
>>> c = HTTPConnection('http2bin.org')
106106
>>> first = c.request('GET', '/get')
107107
>>> second = c.request('POST', '/post', body='key=value')
108108
>>> third = c.request('GET', '/ip')
@@ -112,40 +112,18 @@ For example::
112112

113113
``hyper`` will ensure that each response is matched to the correct request.
114114

115-
Making Your First HTTP/1.1 Request
116-
-----------------------------------
115+
Abstraction
116+
-----------
117117

118-
With ``hyper`` installed, you can start making HTTP/2 requests. At this
119-
stage, ``hyper`` can only be used with services that *definitely* support
120-
HTTP/2. Before you begin, ensure that whichever service you're contacting
121-
definitely supports HTTP/2. For the rest of these examples, we'll use
122-
Twitter.
123-
124-
You can also use ``hyper`` to make HTTP/1.1 requests. The code is very similar.
125-
For example, to get the Twitter homepage::
126-
127-
>>> from hyper import HTTP11Connection
128-
>>> c = HTTP11Connection('twitter.com:443')
129-
>>> c.request('GET', '/')
130-
>>> resp = c.get_response()
131-
132-
The key difference between HTTP/1.1 and HTTP/2 is that when you make HTTP/1.1
133-
requests you do not get a stream ID. This is, of course, because HTTP/1.1 does
134-
not have streams.
118+
When you use the :class:`HTTPConnection <hyper.HTTPConnection>` object, you
119+
don't have to know in advance whether your service supports HTTP/2 or not. If
120+
it doesn't, ``hyper`` will transparently fall back to HTTP/1.1.
135121

136-
Things behave exactly like they do in the HTTP/2 case, right down to the data
137-
reading::
122+
You can tell the difference: if :meth:`request <hyper.HTTPConnection.request>`
123+
returns a stream ID, then the connection is using HTTP/2: if it returns
124+
``None``, then HTTP/1.1 is being used.
138125

139-
>>> resp.headers['content-encoding']
140-
[b'deflate']
141-
>>> resp.headers
142-
HTTPHeaderMap([(b'x-xss-protection', b'1; mode=block')...
143-
>>> resp.status
144-
200
145-
>>> body = resp.read()
146-
b'<!DOCTYPE html>\n<!--[if IE 8]><html clas ....
147-
148-
That's all it takes.
126+
Generally, though, you don't need to care.
149127

150128
Requests Integration
151129
--------------------
@@ -155,8 +133,7 @@ requests doesn't support HTTP/2 though. To rectify that oversight, ``hyper``
155133
provides a transport adapter that can be plugged directly into Requests, giving
156134
it instant HTTP/2 support.
157135

158-
All you have to do is identify a host that you'd like to communicate with over
159-
HTTP/2. Once you've worked that out, you can get started straight away::
136+
Using ``hyper`` with requests is super simple::
160137

161138
>>> import requests
162139
>>> from hyper.contrib import HTTP20Adapter
@@ -169,14 +146,6 @@ HTTP/2. Once you've worked that out, you can get started straight away::
169146
This transport adapter is subject to all of the limitations that apply to
170147
``hyper``, and provides all of the goodness of requests.
171148

172-
A quick warning: some hosts will redirect to new hostnames, which may redirect
173-
you away from HTTP/2. Make sure you install the adapter for all the hostnames
174-
you're interested in::
175-
176-
>>> a = HTTP20Adapter()
177-
>>> s.mount('https://http2bin.org', a)
178-
>>> s.mount('https://www.http2bin.org', a)
179-
180149
.. _requests: http://python-requests.org/
181150

182151
HTTPie Integration

hyper/common/connection.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/common/connection
4+
~~~~~~~~~~~~~~~~~~~~~~~
5+
6+
Hyper's HTTP/1.1 and HTTP/2 abstraction layer.
7+
"""
8+
from .exceptions import TLSUpgrade
9+
from ..http11.connection import HTTP11Connection
10+
from ..http20.connection import HTTP20Connection
11+
from ..tls import H2_NPN_PROTOCOLS
12+
13+
14+
class HTTPConnection(object):
15+
"""
16+
An object representing a single HTTP connection to a server.
17+
18+
This object behaves similarly to the Python standard library's
19+
``HTTPConnection`` object, with a few critical differences.
20+
21+
Most of the standard library's arguments to the constructor are not
22+
supported by hyper. Most optional parameters apply to *either* HTTP/1.1 or
23+
HTTP/2.
24+
25+
:param host: The host to connect to. This may be an IP address or a
26+
hostname, and optionally may include a port: for example,
27+
``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``.
28+
:param port: (optional) The port to connect to. If not provided and one also
29+
isn't provided in the ``host`` parameter, defaults to 443.
30+
:param secure: (optional, HTTP/1.1 only) Whether the request should use
31+
TLS. Defaults to ``False`` for most requests, but to ``True`` for any
32+
request issued to port 443.
33+
:param window_manager: (optional) The class to use to manage flow control
34+
windows. This needs to be a subclass of the
35+
:class:`BaseFlowControlManager <hyper.http20.window.BaseFlowControlManager>`.
36+
If not provided,
37+
:class:`FlowControlManager <hyper.http20.window.FlowControlManager>`
38+
will be used.
39+
:param enable_push: (optional) Whether the server is allowed to push
40+
resources to the client (see
41+
:meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`).
42+
"""
43+
def __init__(self,
44+
host,
45+
port=None,
46+
secure=None,
47+
window_manager=None,
48+
enable_push=False,
49+
**kwargs):
50+
51+
self._host = host
52+
self._port = port
53+
self._h1_kwargs = {'secure': secure}
54+
self._h2_kwargs = {
55+
'window_manager': window_manager, 'enable_push': enable_push
56+
}
57+
58+
# Add any unexpected kwargs to both dictionaries.
59+
self._h1_kwargs.update(kwargs)
60+
self._h2_kwargs.update(kwargs)
61+
62+
self._conn = HTTP11Connection(
63+
self._host, self._port, **self._h1_kwargs
64+
)
65+
66+
def request(self, method, url, body=None, headers={}):
67+
"""
68+
This will send a request to the server using the HTTP request method
69+
``method`` and the selector ``url``. If the ``body`` argument is
70+
present, it should be string or bytes object of data to send after the
71+
headers are finished. Strings are encoded as UTF-8. To use other
72+
encodings, pass a bytes object. The Content-Length header is set to the
73+
length of the body field.
74+
75+
:param method: The request method, e.g. ``'GET'``.
76+
:param url: The URL to contact, e.g. ``'/path/segment'``.
77+
:param body: (optional) The request body to send. Must be a bytestring
78+
or a file-like object.
79+
:param headers: (optional) The headers to send on the request.
80+
:returns: A stream ID for the request, or ``None`` if the request is
81+
made over HTTP/1.1.
82+
"""
83+
try:
84+
return self._conn.request(
85+
method=method, url=url, body=body, headers=headers
86+
)
87+
except TLSUpgrade as e:
88+
# We upgraded in the NPN/ALPN handshake. We can just go straight to
89+
# the world of HTTP/2. Replace the backing object and insert the
90+
# socket into it.
91+
assert e.negotiated in H2_NPN_PROTOCOLS
92+
93+
self._conn = HTTP20Connection(
94+
self._host, self._port, **self._h2_kwargs
95+
)
96+
self._conn._sock = e.sock
97+
98+
# Because we skipped the connecting logic, we need to send the
99+
# HTTP/2 preamble.
100+
self._conn._send_preamble()
101+
102+
return self._conn.request(
103+
method=method, url=url, body=body, headers=headers
104+
)
105+
106+
# Can anyone say 'proxy object pattern'?
107+
def __getattr__(self, name):
108+
return getattr(self._conn, name)

hyper/common/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@ class ConnectionResetError(Exception):
4242
"""
4343
A HTTP connection was unexpectedly reset.
4444
"""
45+
46+
class TLSUpgrade(Exception):
47+
"""
48+
We upgraded to a new protocol in the NPN/ALPN handshake.
49+
"""
50+
def __init__(self, negotiated, sock):
51+
super(TLSUpgrade, self).__init__()
52+
self.negotiated = negotiated
53+
self.sock = sock

hyper/contrib.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
except ImportError: # pragma: no cover
1515
HTTPAdapter = object
1616

17-
from hyper import HTTP20Connection
17+
from hyper.common.connection import HTTPConnection
1818
from hyper.compat import urlparse
1919

20+
2021
class HTTP20Adapter(HTTPAdapter):
2122
"""
2223
A Requests Transport Adapter that uses hyper to send requests over
@@ -27,15 +28,21 @@ def __init__(self, *args, **kwargs):
2728
#: A mapping between HTTP netlocs and ``HTTP20Connection`` objects.
2829
self.connections = {}
2930

30-
def get_connection(self, netloc):
31+
def get_connection(self, host, port, scheme):
3132
"""
32-
Gets an appropriate HTTP/2 connection object based on netloc.
33+
Gets an appropriate HTTP/2 connection object based on host/port/scheme
34+
tuples.
3335
"""
36+
secure = (scheme == 'https')
37+
38+
if port is None: # pragma: no cover
39+
port = 80 if not secure else 443
40+
3441
try:
35-
conn = self.connections[netloc]
42+
conn = self.connections[(host, port, scheme)]
3643
except KeyError:
37-
conn = HTTP20Connection(netloc)
38-
self.connections[netloc] = conn
44+
conn = HTTPConnection(host, port, secure=secure)
45+
self.connections[(host, port, scheme)] = conn
3946

4047
return conn
4148

@@ -45,20 +52,20 @@ def send(self, request, stream=False, **kwargs):
4552
"""
4653
parsed = urlparse(request.url)
4754

48-
conn = self.get_connection(parsed.netloc)
55+
conn = self.get_connection(parsed.hostname, parsed.port, parsed.scheme)
4956

5057
# Build the selector.
5158
selector = parsed.path
5259
selector += '?' + parsed.query if parsed.query else ''
5360
selector += '#' + parsed.fragment if parsed.fragment else ''
5461

55-
stream_id = conn.request(
62+
conn.request(
5663
request.method,
5764
selector,
5865
request.body,
5966
request.headers
6067
)
61-
resp = conn.get_response(stream_id)
68+
resp = conn.get_response()
6269

6370
r = self.build_response(request, resp)
6471

0 commit comments

Comments
 (0)