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

Commit cab873f

Browse files
committed
Support bounded reads of 'expect close' bodies
1 parent 7893fd2 commit cab873f

File tree

2 files changed

+74
-11
lines changed

2 files changed

+74
-11
lines changed

hyper/http11/response.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def read(self, amt=None, decode_content=True):
112112
# Otherwise, we've been asked to do a bounded read. We should read no
113113
# more than the remaining length, obviously.
114114
# FIXME: Handle cases without _length
115-
assert self._length is not None
116-
amt = min(amt, self._length)
115+
if self._length is not None:
116+
amt = min(amt, self._length)
117117

118118
# If we are now going to read nothing, exit early.
119119
if not amt:
@@ -132,28 +132,41 @@ def read(self, amt=None, decode_content=True):
132132
chunk = self._sock.recv(amt).tobytes()
133133

134134
# If we got an empty read, but were expecting more, the remote end
135-
# has hung up. Raise an exception.
135+
# has hung up. Raise an exception if we were expecting more data,
136+
# but if we were expecting the remote end to close then it's ok.
136137
if not chunk:
137-
self.close()
138-
raise ConnectionResetError("Remote end hung up!")
138+
if self._length is not None or not self._expect_close:
139+
self.close()
140+
raise ConnectionResetError("Remote end hung up!")
141+
142+
break
139143

140144
to_read -= len(chunk)
141145
chunks.append(chunk)
142146

143147
data = b''.join(chunks)
144-
self._length -= len(data)
148+
if self._length is not None:
149+
self._length -= len(data)
150+
151+
# If we're at the end of the request, we have some cleaning up to do.
152+
# Close the stream, and if necessary flush the buffer. Checking that
153+
# we're at the end is actually obscenely complex: either we've read the
154+
# full content-length or, if we were expecting a closed connection,
155+
# we've had a read shorter than the requested amount. We also have to
156+
# do this before we try to decompress the body.
157+
end_of_request = (self._length == 0 or
158+
(self._expect_close and len(data) < amt))
145159

146160
# We may need to decode the body.
147161
if decode_content and self._decompressobj and data:
148162
data = self._decompressobj.decompress(data)
149163

150-
# If we're at the end of the request, we have some cleaning up to do.
151-
# Close the stream, and if necessary flush the buffer.
152-
if decode_content and self._decompressobj and not self._length:
164+
if decode_content and self._decompressobj and end_of_request:
153165
data += self._decompressobj.flush()
154166

155-
# We're at the end. Close the connection.
156-
if not self._length:
167+
# We're at the end. Close the connection. Explicit check for zero here
168+
# because self._length might be None.
169+
if end_of_request:
157170
self.close()
158171

159172
return data

test/test_http11.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,56 @@ def test_chunk_length_read(self):
482482
assert r.read(20) == b'reabouts'
483483
assert r.read() == b''
484484

485+
def test_bounded_read_expect_close_no_content_length(self):
486+
d = DummySocket()
487+
r = HTTP11Response(200, 'OK', {b'connection': [b'close']}, d)
488+
d._buffer = BytesIO(b'hello there sir')
489+
490+
assert r.read(5) == b'hello'
491+
assert r.read(6) == b' there'
492+
assert r.read(8) == b' sir'
493+
assert r.read(9) == b''
494+
495+
assert r._sock is None
496+
497+
def test_bounded_read_expect_close_with_content_length(self):
498+
headers = {b'connection': [b'close'], b'content-length': [b'15']}
499+
d = DummySocket()
500+
r = HTTP11Response(200, 'OK', headers, d)
501+
d._buffer = BytesIO(b'hello there sir')
502+
503+
assert r.read(5) == b'hello'
504+
assert r.read(6) == b' there'
505+
assert r.read(8) == b' sir'
506+
assert r.read(9) == b''
507+
508+
assert r._sock is None
509+
510+
def test_compressed_bounded_read_expect_close(self):
511+
headers = {b'connection': [b'close'], b'content-encoding': [b'gzip']}
512+
513+
c = zlib_compressobj(wbits=24)
514+
body = c.compress(b'hello there sir')
515+
body += c.flush()
516+
517+
d = DummySocket()
518+
r = HTTP11Response(200, 'OK', headers, d)
519+
d._buffer = BytesIO(body)
520+
521+
response = b''
522+
while True:
523+
# 12 is magic here: it's the smallest read that actually
524+
# decompresses immediately.
525+
chunk = r.read(15)
526+
if not chunk:
527+
break
528+
529+
response += chunk
530+
531+
assert response == b'hello there sir'
532+
533+
assert r._sock is None
534+
485535
class DummySocket(object):
486536
def __init__(self):
487537
self.queue = []

0 commit comments

Comments
 (0)