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

Add connection/read timeout for requests adapter #342

Merged
merged 9 commits into from
Jul 27, 2017
10 changes: 6 additions & 4 deletions hyper/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, *args, **kwargs):
self.connections = {}

def get_connection(self, host, port, scheme, cert=None, verify=True,
proxy=None):
proxy=None, timeout=None):
"""
Gets an appropriate HTTP/2 connection object based on
host/port/scheme/cert tuples.
Expand Down Expand Up @@ -77,13 +77,14 @@ def get_connection(self, host, port, scheme, cert=None, verify=True,
secure=secure,
ssl_context=ssl_context,
proxy_host=proxy_netloc,
proxy_headers=proxy_headers)
proxy_headers=proxy_headers,
timeout=timeout)
self.connections[connection_key] = conn

return conn

def send(self, request, stream=False, cert=None, verify=True, proxies=None,
**kwargs):
timeout=None, **kwargs):
"""
Sends a HTTP message to the server.
"""
Expand All @@ -98,7 +99,8 @@ def send(self, request, stream=False, cert=None, verify=True, proxies=None,
parsed.scheme,
cert=cert,
verify=verify,
proxy=proxy)
proxy=proxy,
timeout=timeout)

# Build the selector.
selector = parsed.path
Expand Down
18 changes: 15 additions & 3 deletions hyper/http11/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class HTTP11Connection(object):

def __init__(self, host, port=None, secure=None, ssl_context=None,
proxy_host=None, proxy_port=None, proxy_headers=None,
**kwargs):
timeout=None, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add this to the common HTTPConnection object as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,I just added

if port is None:
self.host, self.port = to_host_port_tuple(host, default_port=80)
else:
Expand Down Expand Up @@ -150,6 +150,14 @@ def __init__(self, host, port=None, secure=None, ssl_context=None,
#: the standard hyper parsing interface.
self.parser = Parser()

# timeout
if isinstance(timeout, tuple):
self._connect_timeout = timeout[0]
self._read_timeout = timeout[1]
else:
self._connect_timeout = timeout
self._read_timeout = timeout

def connect(self):
"""
Connect to the server specified when the object was created. This is a
Expand All @@ -172,10 +180,11 @@ def connect(self):
# Simple http proxy
sock = socket.create_connection(
(self.proxy_host, self.proxy_port),
5
timeout=self._connect_timeout
)
else:
sock = socket.create_connection((self.host, self.port), 5)
sock = socket.create_connection((self.host, self.port),
timeout=self._connect_timeout)
proto = None

if self.secure:
Expand All @@ -184,6 +193,9 @@ def connect(self):
log.debug("Selected protocol: %s", proto)
sock = BufferedSocket(sock, self.network_buffer_size)

# Set read timeout
sock.settimeout(self._read_timeout)

if proto not in ('http/1.1', None):
raise TLSUpgrade(proto, sock)

Expand Down
19 changes: 16 additions & 3 deletions hyper/http20/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class HTTP20Connection(object):
def __init__(self, host, port=None, secure=None, window_manager=None,
enable_push=False, ssl_context=None, proxy_host=None,
proxy_port=None, force_proto=None, proxy_headers=None,
**kwargs):
timeout=None, **kwargs):
"""
Creates an HTTP/2 connection to a specific server.
"""
Expand Down Expand Up @@ -151,6 +151,14 @@ def __init__(self, host, port=None, secure=None, window_manager=None,
self.__wm_class = window_manager or FlowControlManager
self.__init_state()

# timeout
if isinstance(timeout, tuple):
self._connect_timeout = timeout[0]
self._read_timeout = timeout[1]
else:
self._connect_timeout = timeout
self._read_timeout = timeout

return

def __init_state(self):
Expand Down Expand Up @@ -355,10 +363,12 @@ def connect(self):
elif self.proxy_host:
# Simple http proxy
sock = socket.create_connection(
(self.proxy_host, self.proxy_port)
(self.proxy_host, self.proxy_port),
timeout=self._connect_timeout
)
else:
sock = socket.create_connection((self.host, self.port))
sock = socket.create_connection((self.host, self.port),
timeout=self._connect_timeout)

if self.secure:
sock, proto = wrap_socket(sock, self.host, self.ssl_context,
Expand All @@ -374,6 +384,9 @@ def connect(self):

self._sock = BufferedSocket(sock, self.network_buffer_size)

# Set read timeout
self._sock.settimeout(self._read_timeout)

self._send_preamble()

def _connect_upgrade(self, sock):
Expand Down
15 changes: 10 additions & 5 deletions test/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ class SocketLevelTest(object):
A test-class that defines a few helper methods for running socket-level
tests.
"""
def set_up(self, secure=True, proxy=False):
def set_up(self, secure=True, proxy=False, timeout=None):
self.host = None
self.port = None
self.socket_security = SocketSecuritySetting(secure)
self.proxy = proxy
self.server_thread = None
self.timeout = timeout

def _start_server(self, socket_handler):
"""
Expand Down Expand Up @@ -146,18 +147,22 @@ def secure(self, value):
def get_connection(self):
if self.h2:
if not self.proxy:
return HTTP20Connection(self.host, self.port, self.secure)
return HTTP20Connection(self.host, self.port, self.secure,
timeout=self.timeout)
else:
return HTTP20Connection('http2bin.org', secure=self.secure,
proxy_host=self.host,
proxy_port=self.port)
proxy_port=self.port,
timeout=self.timeout)
else:
if not self.proxy:
return HTTP11Connection(self.host, self.port, self.secure)
return HTTP11Connection(self.host, self.port, self.secure,
timeout=self.timeout)
else:
return HTTP11Connection('httpbin.org', secure=self.secure,
proxy_host=self.host,
proxy_port=self.port)
proxy_port=self.port,
timeout=self.timeout)

def get_encoder(self):
"""
Expand Down
12 changes: 12 additions & 0 deletions test/test_http11.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ def test_initialization_with_ipv6_addresses_proxy_inline_port(self):
assert c.proxy_host == 'ffff:aaaa::1'
assert c.proxy_port == 8443

def test_initialization_timeout(self):
c = HTTP11Connection('httpbin.org', timeout=30)

assert c._connect_timeout == 30
assert c._read_timeout == 30

def test_initialization_tuple_timeout(self):
c = HTTP11Connection('httpbin.org', timeout=(5, 60))

assert c._connect_timeout == 5
assert c._read_timeout == 60

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need these tests for HTTP/2 as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,I add this in test/test_hyper.py

def test_basic_request(self):
c = HTTP11Connection('httpbin.org')
c._sock = sock = DummySocket()
Expand Down
102 changes: 101 additions & 1 deletion test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import hyper
import hyper.http11.connection
import pytest
from socket import timeout as SocketTimeout
from contextlib import contextmanager
from mock import patch
from concurrent.futures import ThreadPoolExecutor, TimeoutError
Expand Down Expand Up @@ -1230,6 +1231,104 @@ def do_connect(conn):

self.tear_down()

def test_connection_timeout(self):
self.set_up(timeout=0.5)

def socket_handler(listener):
time.sleep(1)
sock = listener.accept()[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No point calling accept: it'll usually fail, and throw exceptions, which we don't want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if connection timeout smaller than 1s, it will throw timeout exception, in this test connection timeout is set to 0.5, so it will throw exception as expect

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but this exception is not caught, meaning it'll be logged and generally treated badly. We shouldn't do anything in the background thread that we know will fail, and accept will fail here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, I just updated

sock.close()

self._start_server(socket_handler)
conn = self.get_connection()
try:
conn.connect()
except (SocketTimeout, ssl.SSLError):
# Py2 raises this as a BaseSSLError,
# Py3 raises it as socket timeout.
# assert 'timed out' in e.message
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we don't need the assert statement in the comment?

pass
else:
assert False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.fail, please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, i'm not familiar with pytest before, i replace it with pytest.raises or pytest.fail

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.raises is the best thing to do if you don't plan to do anything in the except block.


self.tear_down()

def test_read_timeout(self):
self.set_up(timeout=0.5)

req_event = threading.Event()

def socket_handler(listener):
sock = listener.accept()[0]

# We get two messages for the connection open and then a HEADERS
# frame.
receive_preamble(sock)
sock.recv(65535)

# Wait for request
req_event.wait(5)

# Sleep wait for read timeout
time.sleep(1)

sock.close()

self._start_server(socket_handler)
conn = self.get_connection()
conn.request('GET', '/')
req_event.set()

try:
conn.get_response()
except (SocketTimeout, ssl.SSLError):
# Py2 raises this as a BaseSSLError,
# Py3 raises it as socket timeout.
# assert 'timed out' in e.message
pass
else:
assert False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.fail, please.


self.tear_down()

def test_default_connection_timeout(self):
self.set_up(timeout=None)

# Confirm that we send the connection upgrade string and the initial
# SettingsFrame.
data = []
send_event = threading.Event()

def socket_handler(listener):
time.sleep(1)
sock = listener.accept()[0]

# We should get one big chunk.
first = sock.recv(65535)
data.append(first)

# We need to send back a SettingsFrame.
f = SettingsFrame(0)
sock.send(f.serialize())

send_event.set()
sock.close()

self._start_server(socket_handler)
conn = self.get_connection()
try:
conn.connect()
except (SocketTimeout, ssl.SSLError):
# Py2 raises this as a BaseSSLError,
# Py3 raises it as socket timeout.
assert False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest.fail, please.


send_event.wait(5)

assert data[0].startswith(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can this possibly pass? If the connection fails, we're never going to see this data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default timeout None will block connect, so it will connect sucess and go on

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, I see that. Thanks!


self.tear_down()


@patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS)
class TestRequestsAdapter(SocketLevelTest):
Expand Down Expand Up @@ -1290,7 +1389,8 @@ def socket_handler(listener):

s = requests.Session()
s.mount('https://%s' % self.host, HTTP20Adapter())
r = s.get('https://%s:%s/some/path' % (self.host, self.port))
r = s.get('https://%s:%s/some/path' % (self.host, self.port),
timeout=(10, 60))

# Assert about the received values.
assert r.status_code == 200
Expand Down