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

Commit 461bbbc

Browse files
committed
Add support for HTTP/1.1 bodies.
1 parent f147d3f commit 461bbbc

File tree

2 files changed

+251
-15
lines changed

2 files changed

+251
-15
lines changed

hyper/http11/connection.py

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
66
Objects that build hyper's connection-level HTTP/1.1 abstraction.
77
"""
8-
import io
98
import logging
10-
import re
9+
import os
1110
import socket
1211

1312
from .response import HTTP11Response
1413
from ..http20.bufsocket import BufferedSocket
1514
from ..common.headers import HTTPHeaderMap
1615
from ..common.util import to_bytestring
16+
from ..compat import bytes
1717

1818
log = logging.getLogger(__name__)
1919

20+
BODY_CHUNKED = 1
21+
BODY_FLAT = 2
22+
2023

2124
class HTTP11Connection(object):
2225
"""
@@ -91,24 +94,24 @@ def request(self, method, url, body=None, headers={}):
9194
method = to_bytestring(method)
9295
url = to_bytestring(url)
9396

97+
if not isinstance(headers, HTTPHeaderMap):
98+
# FIXME: Handle things that aren't dictionaries here.
99+
headers = HTTPHeaderMap(headers.items())
100+
94101
if self._sock is None:
95102
self.connect()
96103

97-
# In this initial implementation, let's just write straight to the
98-
# socket. We'll fix this up as we go.
99-
# TODO: Fix fix fix.
100-
self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n']))
101-
102-
for name, value in headers.items():
103-
name, value = to_bytestring(name), to_bytestring(value)
104-
header = b''.join([name, b': ', value, b'\r\n'])
105-
self._sock.send(header)
104+
# We may need extra headers. For now, we only add headers based on
105+
# body content.
106+
if body:
107+
body_type = self._add_body_headers(headers, body)
106108

107-
self._sock.send(b'\r\n')
109+
# Begin by emitting the header block.
110+
self._send_headers(method, url, headers)
108111

112+
# Next, send the request body.
109113
if body:
110-
# TODO: Come back here to support non-string bodies.
111-
self._sock.send(body)
114+
self._send_body(body, body_type)
112115

113116
return
114117

@@ -136,3 +139,107 @@ def get_response(self):
136139
headers[name] = val
137140

138141
return HTTP11Response(code, reason, headers, self._sock)
142+
143+
def _send_headers(self, method, url, headers):
144+
"""
145+
Handles the logic of sending the header block.
146+
"""
147+
self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n']))
148+
149+
for name, value in headers.iter_raw():
150+
name, value = to_bytestring(name), to_bytestring(value)
151+
header = b''.join([name, b': ', value, b'\r\n'])
152+
self._sock.send(header)
153+
154+
self._sock.send(b'\r\n')
155+
156+
def _add_body_headers(self, headers, body):
157+
"""
158+
Adds any headers needed for sending the request body. This will always
159+
defer to the user-supplied header content.
160+
161+
:returns: One of (BODY_CHUNKED, BODY_FLAT), indicating what type of
162+
request body should be used.
163+
"""
164+
if b'content-length' in headers:
165+
return BODY_FLAT
166+
167+
if b'chunked' in headers.get(b'transfer-encoding', []):
168+
return BODY_CHUNKED
169+
170+
# For bytestring bodies we upload the content with a fixed length.
171+
# For file objects, we use the length of the file object.
172+
if isinstance(body, bytes):
173+
length = str(len(body)).encode('utf-8')
174+
elif hasattr(body, 'fileno'):
175+
length = str(os.fstat(body.fileno()).st_size).encode('utf-8')
176+
else:
177+
length = None
178+
179+
if length:
180+
headers[b'content-length'] = length
181+
return BODY_FLAT
182+
183+
headers[b'transfer-encoding'] = b'chunked'
184+
return BODY_CHUNKED
185+
186+
def _send_body(self, body, body_type):
187+
"""
188+
Handles the HTTP/1.1 logic for sending HTTP bodies. This does magical
189+
different things in different cases.
190+
"""
191+
if body_type == BODY_FLAT:
192+
# Special case for files and other 'readable' objects.
193+
if hasattr(body, 'read'):
194+
while True:
195+
block = body.read(16*1024)
196+
if not block:
197+
break
198+
199+
try:
200+
self._sock.send(block)
201+
except TypeError:
202+
raise ValueError(
203+
"File objects must return bytestrings"
204+
)
205+
206+
return
207+
208+
# Case for bytestrings.
209+
elif isinstance(body, bytes):
210+
try:
211+
self._sock.send(body)
212+
except TypeError:
213+
raise ValueError("Body must be a bytestring")
214+
215+
return
216+
217+
# Iterables that set a specific content length.
218+
else:
219+
for item in body:
220+
try:
221+
self._sock.send(item)
222+
except TypeError:
223+
raise ValueError("Body must be a bytestring")
224+
225+
return
226+
227+
# Chunked! For chunked bodies we don't special-case, we just iterate
228+
# over what we have and send stuff out.
229+
for chunk in body:
230+
length = '{0:x}'.format(len(chunk)).encode('ascii')
231+
232+
# For now write this as four 'send' calls. That's probably
233+
# inefficient, let's come back to it.
234+
try:
235+
self._sock.send(length)
236+
self._sock.send(b'\r\n')
237+
self._sock.send(chunk)
238+
self._sock.send(b'\r\n')
239+
except TypeError:
240+
raise ValueError(
241+
"Iterable bodies must always iterate in bytestrings"
242+
)
243+
244+
self._sock.send(b'0\r\n\r\n')
245+
return

test/test_http11.py

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
66
Unit tests for hyper's HTTP/1.1 implementation.
77
"""
8+
from collections import namedtuple
89
from io import BytesIO
910

11+
import hyper
1012
from hyper.http11.connection import HTTP11Connection
1113
from hyper.http11.response import HTTP11Response
14+
from hyper.common.headers import HTTPHeaderMap
1215

1316

1417
class TestHTTP11Connection(object):
@@ -52,18 +55,126 @@ def test_request_with_bytestring_body(self):
5255
c = HTTP11Connection('http2bin.org')
5356
c._sock = sock = DummySocket()
5457

55-
c.request('POST', '/post', headers={'User-Agent': 'hyper'}, body=b'hi')
58+
c.request(
59+
'POST',
60+
'/post',
61+
headers=HTTPHeaderMap([('User-Agent', 'hyper')]),
62+
body=b'hi'
63+
)
5664

5765
expected = (
5866
b"POST /post HTTP/1.1\r\n"
5967
b"User-Agent: hyper\r\n"
68+
b"content-length: 2\r\n"
6069
b"\r\n"
6170
b"hi"
6271
)
6372
received = b''.join(sock.queue)
6473

6574
assert received == expected
6675

76+
def test_request_with_file_body(self):
77+
# Testing this is tricksy: in practice, we do this by passing a fake
78+
# file and monkeypatching out 'os.fstat'. This makes it look like a
79+
# real file.
80+
FstatRval = namedtuple('FstatRval', ['st_size'])
81+
def fake_fstat(*args):
82+
return FstatRval(16)
83+
84+
old_fstat = hyper.http11.connection.os.fstat
85+
86+
try:
87+
hyper.http11.connection.os.fstat = fake_fstat
88+
c = HTTP11Connection('http2bin.org')
89+
c._sock = sock = DummySocket()
90+
91+
f = DummyFile(b'some binary data')
92+
c.request('POST', '/post', body=f)
93+
94+
expected = (
95+
b"POST /post HTTP/1.1\r\n"
96+
b"content-length: 16\r\n"
97+
b"\r\n"
98+
b"some binary data"
99+
)
100+
received = b''.join(sock.queue)
101+
102+
assert received == expected
103+
104+
finally:
105+
# Put back the monkeypatch.
106+
hyper.http11.connection.os.fstat = old_fstat
107+
108+
def test_request_with_generator_body(self):
109+
c = HTTP11Connection('http2bin.org')
110+
c._sock = sock = DummySocket()
111+
def body():
112+
yield b'hi'
113+
yield b'there'
114+
yield b'sir'
115+
116+
c.request('POST', '/post', body=body())
117+
118+
expected = (
119+
b"POST /post HTTP/1.1\r\n"
120+
b"transfer-encoding: chunked\r\n"
121+
b"\r\n"
122+
b"2\r\nhi\r\n"
123+
b"5\r\nthere\r\n"
124+
b"3\r\nsir\r\n"
125+
b"0\r\n\r\n"
126+
)
127+
received = b''.join(sock.queue)
128+
129+
assert received == expected
130+
131+
def test_content_length_overrides_generator(self):
132+
c = HTTP11Connection('http2bin.org')
133+
c._sock = sock = DummySocket()
134+
def body():
135+
yield b'hi'
136+
yield b'there'
137+
yield b'sir'
138+
139+
c.request(
140+
'POST', '/post', headers={b'content-length': b'10'}, body=body()
141+
)
142+
143+
expected = (
144+
b"POST /post HTTP/1.1\r\n"
145+
b"content-length: 10\r\n"
146+
b"\r\n"
147+
b"hitheresir"
148+
)
149+
received = b''.join(sock.queue)
150+
151+
assert received == expected
152+
153+
def test_chunked_overrides_body(self):
154+
c = HTTP11Connection('http2bin.org')
155+
c._sock = sock = DummySocket()
156+
157+
f = DummyFile(b'oneline\nanotherline')
158+
159+
c.request(
160+
'POST',
161+
'/post',
162+
headers={'transfer-encoding': 'chunked'},
163+
body=f
164+
)
165+
166+
expected = (
167+
b"POST /post HTTP/1.1\r\n"
168+
b"transfer-encoding: chunked\r\n"
169+
b"\r\n"
170+
b"8\r\noneline\n\r\n"
171+
b"b\r\nanotherline\r\n"
172+
b"0\r\n\r\n"
173+
)
174+
received = b''.join(sock.queue)
175+
176+
assert received == expected
177+
67178
def test_get_response(self):
68179
c = HTTP11Connection('http2bin.org')
69180
c._sock = sock = DummySocket()
@@ -132,3 +243,21 @@ def close(self):
132243

133244
def readline(self):
134245
return memoryview(self.buffer.readline())
246+
247+
248+
class DummyFile(object):
249+
def __init__(self, data):
250+
self.buffer = BytesIO(data)
251+
252+
def read(self, amt=None):
253+
return self.buffer.read(amt)
254+
255+
def fileno(self):
256+
return -1
257+
258+
def readline(self):
259+
self.buffer.readline()
260+
261+
def __iter__(self):
262+
return self.buffer.__iter__()
263+

0 commit comments

Comments
 (0)