timeout_handle = self.io_loop.add_timeout(
self.io_loop.time() + min(request.connect_timeout,
request.request_timeout),
- functools.partial(self._on_timeout, key))
+ functools.partial(self._on_timeout, key, "in request queue"))
else:
timeout_handle = None
self.waiting[key] = (request, callback, timeout_handle)
self.io_loop.remove_timeout(timeout_handle)
del self.waiting[key]
- def _on_timeout(self, key):
+ def _on_timeout(self, key, info=None):
+ """Timeout callback of request.
+
+ Construct a timeout HTTPResponse when a timeout occurs.
+
+ :arg object key: A simple object to mark the request.
+ :info string key: More detailed timeout information.
+ """
request, callback, timeout_handle = self.waiting[key]
self.queue.remove((key, request, callback))
+
+ error_message = "Timeout {0}".format(info) if info else "Timeout"
timeout_response = HTTPResponse(
- request, 599, error=HTTPError(599, "Timeout"),
+ request, 599, error=HTTPError(599, error_message),
request_time=self.io_loop.time() - request.start_time)
self.io_loop.add_callback(callback, timeout_response)
del self.waiting[key]
if timeout:
self._timeout = self.io_loop.add_timeout(
self.start_time + timeout,
- stack_context.wrap(self._on_timeout))
+ stack_context.wrap(functools.partial(self._on_timeout, "while connecting")))
self.tcp_client.connect(host, port, af=af,
ssl_options=ssl_options,
max_buffer_size=self.max_buffer_size,
return ssl_options
return None
- def _on_timeout(self):
+ def _on_timeout(self, info=None):
+ """Timeout callback of _HTTPConnection instance.
+
+ Raise a timeout HTTPError when a timeout occurs.
+
+ :info string key: More detailed timeout information.
+ """
self._timeout = None
+ error_message = "Timeout {0}".format(info) if info else "Timeout"
if self.final_callback is not None:
- raise HTTPError(599, "Timeout")
+ raise HTTPError(599, error_message)
def _remove_timeout(self):
if self._timeout is not None:
if self.request.request_timeout:
self._timeout = self.io_loop.add_timeout(
self.start_time + self.request.request_timeout,
- stack_context.wrap(self._on_timeout))
+ stack_context.wrap(functools.partial(self._on_timeout, "while interacting")))
if (self.request.method not in self._SUPPORTED_METHODS and
not self.request.allow_nonstandard_methods):
raise KeyError("unknown method %s" % self.request.method)
import socket
import ssl
import sys
+import time
from tornado.escape import to_unicode
from tornado import gen
from tornado.httputil import HTTPHeaders, ResponseStartLine
from tornado.ioloop import IOLoop
from tornado.log import gen_log
-from tornado.netutil import Resolver, bind_sockets
-from tornado.simple_httpclient import SimpleAsyncHTTPClient
+from tornado.netutil import Resolver, bind_sockets, BlockingResolver
+from tornado.simple_httpclient import SimpleAsyncHTTPClient, _HTTPConnection
from tornado.test.httpclient_test import ChunkHandler, CountdownHandler, HelloWorldHandler, RedirectHandler
from tornado.test import httpclient_test
from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, ExpectLog
# request is the original request, is a POST still
self.assertEqual("POST", response.request.method)
+ @skipOnTravis
+ def test_connect_timeout(self):
+ # 0.1 and 0.5(nt) is copied from `self.test_request_timeout`
+ # `timeout_max` is raised up compare to `self.test_request_timeout`.
+ #
+ # Because at windows system, in `TimeoutResolver.resolve` call, if
+ # `time.sleep's parameter` is the same as the `connect_timeout` value,
+ # maybe no error occurs and everything works well(and we get response
+ # 200 instead of 599). So `time.sleep's parameter` should be bigger
+ # than the `connect_timeout`(add 0.2), that's why the `timeout_max` is
+ # raised up(add 0.4).
+ connect_timeout = 0.1
+ timeout_min, timeout_max = 0.099, 0.5
+ if os.name == 'nt':
+ connect_timeout = 0.5
+ timeout_min, timeout_max = 0.4, 0.9
+ sleep_seconds = connect_timeout + 0.2
+
+ class patching(object):
+ # In `simple_httpclient._HTTPConnection.__init__`:
+ # We add a connect timeout to io_loop and then begin our
+ # `self.tcp_connect.connect` step.
+ #
+ # If connect timeout is triggered, but `self.tcp_client.connect`
+ # call is still in progress, at this moment: an error may occur
+ # while connecting(yield from `self.tcp_client.connect` at
+ # `stream.start_tls`, which is mostly caused by
+ # `SSLIOStream._do_ssl_handshake` call), since connection is not
+ # finished because of the exception, the self._on_connect will not
+ # be called. The exception(`ssl.SSL_ERROR_EOF`,
+ # `errno.ECONNRESET`, `errno.WSAECONNABORTED`) will be handled by
+ # the stack_context's exception handler.
+ #
+ # `_HTTPConnection` has add exception handler
+ # `_HTTPConnection._handle_exception` to stack_context.
+ # And in `simple_httpclient.SimpleAsyncHTTPClient`, we cleaned all
+ # context in
+ # `simple_httpclient.SimpleAsyncHTTPClient._process_queue` before
+ # handling any request. So, if we want to handle the exception
+ # raised by `self.tcp_client.connect`(connection is not finished),
+ # we have to add a context(or patch a context) after
+ # `simple_httpclient.SimpleAsyncHTTPClient._process_queue` call
+ # and before `self.tcp_client.connect` call.
+
+ def __init__(self, client):
+ self.client = client
+
+ def __enter__(self):
+ def _patched_handle_exception(instance, typ, value, tb):
+ """Handle extra exception while connection failed.
+
+ `ssl.SSL_ERROR_EOF`, `errno.ECONNRESET` and
+ `errno.WSAECONNABORTED`(windows) may occur if connection
+ failed(mostly in `SSLIOStream._do_ssl_handshake`), just
+ ignore them. Other logic is the same as
+ `_HTTPConnection._handle_exception`
+ """
+ result = self._origin_handle_exception(instance, typ, value, tb)
+ if result is False:
+ if isinstance(value, ssl.SSLError) \
+ and value.args[0] == ssl.SSL_ERROR_EOF:
+ return True
+
+ ignored_io_error = [errno.ECONNRESET]
+ if hasattr(errno, "WSAECONNRESET"):
+ ignored_io_error.append(errno.WSAECONNABORTED)
+ if isinstance(value, IOError) \
+ and value.args[0] in ignored_io_error:
+ return True
+ return result
+
+ self._origin_handle_exception = _HTTPConnection._handle_exception
+ _HTTPConnection._handle_exception = _patched_handle_exception
+ return self.client
+
+ def __exit__(self, *exc_info):
+ _HTTPConnection._handle_exception = self._origin_handle_exception
+ self.client.close()
+
+ class TimeoutResolver(BlockingResolver):
+ def resolve(self, *args, **kwargs):
+ time.sleep(sleep_seconds)
+ return super(TimeoutResolver, self).resolve(*args, **kwargs)
+
+ with patching(self.create_client(resolver=TimeoutResolver())) as client:
+ client.fetch(self.get_url('/hello'), self.stop,
+ connect_timeout=connect_timeout)
+ response = self.wait()
+ self.assertEqual(response.code, 599)
+ self.assertTrue(timeout_min < response.request_time < timeout_max,
+ response.request_time)
+ self.assertEqual(str(response.error), "HTTP 599: Timeout while connecting")
+
@skipOnTravis
def test_request_timeout(self):
timeout = 0.1
self.assertEqual(response.code, 599)
self.assertTrue(timeout_min < response.request_time < timeout_max,
response.request_time)
- self.assertEqual(str(response.error), "HTTP 599: Timeout")
+ self.assertEqual(str(response.error), "HTTP 599: Timeout while interacting")
# trigger the hanging request to let it clean up after itself
self.triggers.popleft()()
self.assertEqual(response.code, 599)
self.assertTrue(response.request_time < 1, response.request_time)
- self.assertEqual(str(response.error), "HTTP 599: Timeout")
+ self.assertEqual(str(response.error), "HTTP 599: Timeout in request queue")
self.triggers.popleft()()
self.wait()