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

Commit 88da420

Browse files
committed
Support chunked reads of HTTP/2.
1 parent c3f24dd commit 88da420

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

hyper/http20/response.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,34 @@ def read(self, amt=None, decode_content=True):
135135

136136
return data
137137

138+
def read_chunked(self, decode_content=True):
139+
"""
140+
Reads chunked transfer encoded bodies. This method returns a generator:
141+
each iteration of which yields one data frame *unless* the frames
142+
contain compressed data and ``decode_content`` is ``True``, in which
143+
case it yields whatever the decompressor provides for each chunk.
144+
145+
.. warning:: This may yield the empty string, without that being the
146+
end of the body!
147+
"""
148+
while True:
149+
data = self._stream._read_one_frame()
150+
151+
if data is None:
152+
break
153+
154+
if decode_content and self._decompressobj:
155+
data = self._decompressobj.decompress(data)
156+
157+
yield data
158+
159+
if decode_content and self._decompressobj:
160+
yield self._decompressobj.flush()
161+
162+
self.close()
163+
164+
return
165+
138166
def fileno(self):
139167
"""
140168
Return the ``fileno`` of the underlying socket. This function is

hyper/http20/stream.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,19 @@ def listlen(list):
179179
self.data = []
180180
return result
181181

182+
def _read_one_frame(self):
183+
"""
184+
Reads a single data frame from the stream and returns it.
185+
"""
186+
# Keep reading until the stream is closed or we have a data frame.
187+
while not self._remote_closed and not self.data:
188+
self._recv_cb()
189+
190+
try:
191+
return self.data.pop(0)
192+
except IndexError:
193+
return None
194+
182195
def receive_frame(self, frame):
183196
"""
184197
Handle a frame received on this stream.

test/test_hyper.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,6 +1861,44 @@ def inner():
18611861
assert s.gettrailers() == HTTPHeaderMap(trailers)
18621862
assert s.data == [b'testdata']
18631863

1864+
def test_can_read_single_frames_from_streams(self):
1865+
out_frames = []
1866+
in_frames = []
1867+
1868+
def send_cb(frame, tolerate_peer_gone=False):
1869+
out_frames.append(frame)
1870+
1871+
def recv_cb(s):
1872+
def inner():
1873+
s.receive_frame(in_frames.pop(0))
1874+
return inner
1875+
1876+
s = Stream(1, send_cb, None, None, None, None, FlowControlManager(800))
1877+
s._recv_cb = recv_cb(s)
1878+
s.state = STATE_HALF_CLOSED_LOCAL
1879+
1880+
# Provide two data frames to read.
1881+
f = DataFrame(1)
1882+
f.data = b'hi there!'
1883+
in_frames.append(f)
1884+
1885+
f = DataFrame(1)
1886+
f.data = b'hi there again!'
1887+
f.flags.add('END_STREAM')
1888+
in_frames.append(f)
1889+
1890+
data = s._read_one_frame()
1891+
assert data == b'hi there!'
1892+
1893+
data = s._read_one_frame()
1894+
assert data == b'hi there again!'
1895+
1896+
data = s._read_one_frame()
1897+
assert data is None
1898+
1899+
data = s._read()
1900+
assert data == b''
1901+
18641902

18651903
class TestResponse(object):
18661904
def test_status_is_stripped_from_headers(self):
@@ -1968,6 +2006,33 @@ def test_trailers_are_read(self):
19682006
assert resp.trailers['a'] == [b'b']
19692007
assert resp.trailers['c'] == [b'd']
19702008

2009+
def test_read_frames(self):
2010+
headers = HTTPHeaderMap([(':status', '200')])
2011+
stream = DummyStream(None)
2012+
chunks = [b'12', b'3456', b'78', b'9']
2013+
stream.data_frames = chunks
2014+
resp = HTTP20Response(headers, stream)
2015+
2016+
for recv, expected in zip(resp.read_chunked(), chunks[:]):
2017+
assert recv == expected
2018+
2019+
def test_read_compressed_frames(self):
2020+
headers = HTTPHeaderMap([(':status', '200'), ('content-encoding', 'gzip')])
2021+
c = zlib_compressobj(wbits=24)
2022+
body = c.compress(b'this is test data')
2023+
body += c.flush()
2024+
2025+
stream = DummyStream(None)
2026+
chunks = [body[x:x+2] for x in range(0, len(body), 2)]
2027+
stream.data_frames = chunks
2028+
resp = HTTP20Response(headers, stream)
2029+
2030+
received = b''
2031+
for chunk in resp.read_chunked():
2032+
received += chunk
2033+
2034+
assert received == b'this is test data'
2035+
19712036

19722037
class TestHTTP20Adapter(object):
19732038
def test_adapter_reuses_connections(self):
@@ -2090,6 +2155,7 @@ def recv(self, l):
20902155
class DummyStream(object):
20912156
def __init__(self, data, trailers=None):
20922157
self.data = data
2158+
self.data_frames = []
20932159
self.closed = False
20942160
self.response_headers = {}
20952161
self._remote_closed = False
@@ -2112,6 +2178,12 @@ def _read(self, *args, **kwargs):
21122178

21132179
return d
21142180

2181+
def _read_one_frame(self):
2182+
try:
2183+
return self.data_frames.pop(0)
2184+
except IndexError:
2185+
return None
2186+
21152187
def close(self):
21162188
if not self.closed:
21172189
self.closed = True

0 commit comments

Comments
 (0)