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

Commit 683068f

Browse files
authored
Merge pull request #257 from 72squared/conn_close
potential fix for multiple stream reads when sock closes
2 parents 8100175 + 4d92624 commit 683068f

File tree

2 files changed

+106
-5
lines changed

2 files changed

+106
-5
lines changed

hyper/http20/connection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ def _single_read(self):
641641
#
642642
# I/O occurs while the lock is held; waiting threads will see a delay.
643643
with self._read_lock:
644+
if self._sock is None:
645+
raise ConnectionError('tried to read after connection close')
644646
self._sock.fill()
645647
data = self._sock.buffer.tobytes()
646648
self._sock.advance_buffer(len(data))
@@ -740,6 +742,12 @@ def _recv_cb(self, stream_id=0):
740742
self.recent_recv_streams.discard(stream_id)
741743
return
742744

745+
# make sure to validate the stream is readable.
746+
# if the connection was reset, this stream id won't appear in
747+
# self.streams and will cause this call to raise an exception.
748+
if stream_id:
749+
self._get_stream(stream_id)
750+
743751
# TODO: Re-evaluate this.
744752
self._single_read()
745753
count = 9

test/test_hyper.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from hpack.hpack_compat import Encoder
99
from hyper.http20.connection import HTTP20Connection
1010
from hyper.http20.response import HTTP20Response, HTTP20Push
11-
from hyper.http20.exceptions import ConnectionError
11+
from hyper.http20.exceptions import ConnectionError, StreamResetError
1212
from hyper.http20.util import (
1313
combine_repeated_headers, split_repeated_headers, h2_safe_headers
1414
)
@@ -23,7 +23,6 @@
2323
import zlib
2424
from io import BytesIO
2525

26-
2726
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
2827
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
2928
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
@@ -293,6 +292,100 @@ def test_streams_are_cleared_from_connections_on_close(self):
293292
assert not c.streams
294293
assert c.next_stream_id == 3
295294

295+
def test_streams_raise_error_on_read_after_close(self):
296+
# Prepare a socket so we can open a stream.
297+
sock = DummySocket()
298+
c = HTTP20Connection('www.google.com')
299+
c._sock = sock
300+
301+
# Open a request (which creates a stream)
302+
stream_id = c.request('GET', '/')
303+
304+
# close connection
305+
c.close()
306+
307+
# try to read the stream
308+
with pytest.raises(StreamResetError):
309+
c.get_response(stream_id)
310+
311+
def test_reads_on_remote_close(self):
312+
# Prepare a socket so we can open a stream.
313+
sock = DummySocket()
314+
c = HTTP20Connection('www.google.com')
315+
c._sock = sock
316+
317+
# Open a few requests (which creates a stream)
318+
s1 = c.request('GET', '/')
319+
s2 = c.request('GET', '/')
320+
321+
# simulate state of blocking on read while sock
322+
f = GoAwayFrame(0)
323+
# Set error code to PROTOCOL_ERROR
324+
f.error_code = 1
325+
c._sock.buffer = BytesIO(f.serialize())
326+
327+
# 'Receive' the GOAWAY frame.
328+
# Validate that the spec error name and description are used to throw
329+
# the connection exception.
330+
with pytest.raises(ConnectionError):
331+
c.get_response(s1)
332+
333+
# try to read the stream
334+
with pytest.raises(StreamResetError):
335+
c.get_response(s2)
336+
337+
def test_race_condition_on_socket_close(self):
338+
# Prepare a socket so we can open a stream.
339+
sock = DummySocket()
340+
c = HTTP20Connection('www.google.com')
341+
c._sock = sock
342+
343+
# Open a few requests (which creates a stream)
344+
s1 = c.request('GET', '/')
345+
c.request('GET', '/')
346+
347+
# simulate state of blocking on read while sock
348+
f = GoAwayFrame(0)
349+
# Set error code to PROTOCOL_ERROR
350+
f.error_code = 1
351+
c._sock.buffer = BytesIO(f.serialize())
352+
353+
# 'Receive' the GOAWAY frame.
354+
# Validate that the spec error name and description are used to throw
355+
# the connection exception.
356+
with pytest.raises(ConnectionError):
357+
c.get_response(s1)
358+
359+
# try to read again after close
360+
with pytest.raises(ConnectionError):
361+
c._single_read()
362+
363+
def test_stream_close_behavior(self):
364+
# Prepare a socket so we can open a stream.
365+
sock = DummySocket()
366+
c = HTTP20Connection('www.google.com')
367+
c._sock = sock
368+
369+
# Open a few requests (which creates a stream)
370+
s1 = c.request('GET', '/')
371+
c.request('GET', '/')
372+
373+
# simulate state of blocking on read while sock
374+
f = GoAwayFrame(0)
375+
# Set error code to PROTOCOL_ERROR
376+
f.error_code = 1
377+
c._sock.buffer = BytesIO(f.serialize())
378+
379+
# 'Receive' the GOAWAY frame.
380+
# Validate that the spec error name and description are used to throw
381+
# the connection exception.
382+
with pytest.raises(ConnectionError):
383+
c.get_response(s1)
384+
385+
# try to read again after close
386+
with pytest.raises(ConnectionError):
387+
c._single_read()
388+
296389
def test_read_headers_out_of_order(self):
297390
# If header blocks aren't decoded in the same order they're received,
298391
# regardless of the stream they belong to, the decoder state will
@@ -326,10 +419,10 @@ def test_headers_with_continuation(self):
326419
('content-length', '0')
327420
])
328421
h = HeadersFrame(1)
329-
h.data = header_data[0:int(len(header_data)/2)]
422+
h.data = header_data[0:int(len(header_data) / 2)]
330423
h.flags.add('END_STREAM')
331424
c = ContinuationFrame(1)
332-
c.data = header_data[int(len(header_data)/2):]
425+
c.data = header_data[int(len(header_data) / 2):]
333426
c.flags.add('END_HEADERS')
334427
sock = DummySocket()
335428
sock.buffer = BytesIO(h.serialize() + c.serialize())
@@ -883,7 +976,7 @@ def test_read_compressed_frames(self):
883976
body += c.flush()
884977

885978
stream = DummyStream(None)
886-
chunks = [body[x:x+2] for x in range(0, len(body), 2)]
979+
chunks = [body[x:x + 2] for x in range(0, len(body), 2)]
887980
stream.data_frames = chunks
888981
resp = HTTP20Response(headers, stream)
889982

0 commit comments

Comments
 (0)