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

Commit c72f4da

Browse files
committed
Chunked responses
1 parent 56fa068 commit c72f4da

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

hyper/common/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/common/exceptions
4+
~~~~~~~~~~~~~~~~~~~~~~~
5+
6+
Contains hyper's exceptions.
7+
"""
8+
class ChunkedDecodeError(Exception):
9+
"""
10+
An error was encountered while decoding a chunked response.
11+
"""
12+
pass

hyper/http11/response.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import zlib
1111

1212
from ..common.decoder import DeflateDecoder
13+
from ..common.exceptions import ChunkedDecodeError
1314
from ..http20.exceptions import ConnectionResetError
1415

1516
log = logging.getLogger(__name__)
@@ -52,7 +53,7 @@ def __init__(self, code, reason, headers, sock):
5253
self._length = None
5354

5455
# Whether we expect a chunked response.
55-
self._chunked = b'chunked' in self.headers.get('transfer-encoding', [])
56+
self._chunked = b'chunked' in self.headers.get(b'transfer-encoding', [])
5657

5758
# One of the following must be true: we must expect that the connection
5859
# will be closed following the body, or that a content-length was sent,
@@ -153,6 +154,44 @@ def read(self, amt=None, decode_content=True):
153154

154155
return data
155156

157+
def read_chunked(self):
158+
"""
159+
Reads chunked transfer encoded bodies. Each read returns a single
160+
chunk.
161+
"""
162+
if not self._chunked:
163+
raise ChunkedDecodeError(
164+
"Attempted chunked read of non-chunked body."
165+
)
166+
167+
# Return early if possible.
168+
if self._sock is None:
169+
return b''
170+
171+
# Read to the newline to get the chunk length. This is a hexadecimal
172+
# integer.
173+
chunk_length = int(self._sock.readline().tobytes().strip(), 16)
174+
175+
# If the chunk length is zero, consume the newline and then we're done.
176+
if not chunk_length:
177+
self._sock.readline()
178+
return b''
179+
180+
# Then read that many bytes.
181+
data = b''
182+
183+
while chunk_length > 0:
184+
chunk = self._sock.recv(chunk_length).tobytes()
185+
data += chunk
186+
chunk_length -= len(chunk)
187+
188+
assert chunk_length == 0
189+
190+
# Now, consume the newline.
191+
self._sock.readline()
192+
193+
return data
194+
156195
def close(self):
157196
"""
158197
Close the response. In effect this closes the backing socket.

test/test_http11.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from hyper.http11.response import HTTP11Response
1919
from hyper.http20.exceptions import ConnectionResetError
2020
from hyper.common.headers import HTTPHeaderMap
21+
from hyper.common.exceptions import ChunkedDecodeError
2122
from hyper.compat import bytes, zlib_compressobj
2223

2324

@@ -383,6 +384,43 @@ def test_response_transparently_decrypts_wrong_deflate(self):
383384

384385
assert r.read() == b'this is test data'
385386

387+
def test_basic_chunked_read(self):
388+
d = DummySocket()
389+
r = HTTP11Response(200, 'OK', {b'transfer-encoding': [b'chunked']}, d)
390+
391+
data = (
392+
b'4\r\nwell\r\n'
393+
b'4\r\nwell\r\n'
394+
b'4\r\nwhat\r\n'
395+
b'4\r\nhave\r\n'
396+
b'2\r\nwe\r\n'
397+
b'a\r\nhereabouts\r\n'
398+
b'0\r\n\r\n'
399+
)
400+
d._buffer = BytesIO(data)
401+
results = [
402+
b'well', b'well', b'what', b'have', b'we', b'hereabouts'
403+
]
404+
405+
for c in results:
406+
assert r.read_chunked() == c
407+
408+
assert not r.read_chunked()
409+
410+
def test_chunked_read_of_non_chunked(self):
411+
r = HTTP11Response(200, 'OK', {b'content-length': [b'0']}, None)
412+
413+
with pytest.raises(ChunkedDecodeError):
414+
r.read_chunked()
415+
416+
def test_chunked_read_aborts_early(self):
417+
r = HTTP11Response(
418+
200, 'OK', {b'transfer-encoding': [b'chunked']}, None
419+
)
420+
421+
assert not r.read_chunked()
422+
423+
386424
class DummySocket(object):
387425
def __init__(self):
388426
self.queue = []

0 commit comments

Comments
 (0)