]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
add more detailed information to HTTP Timeout error raised by SimpleAsyncHTTPClient
authorColor Fuzzy <color.fuzzy@gmail.com>
Sat, 19 Dec 2015 19:34:07 +0000 (03:34 +0800)
committerColor Fuzzy <color.fuzzy@gmail.com>
Mon, 21 Dec 2015 04:10:35 +0000 (12:10 +0800)
tornado/simple_httpclient.py
tornado/test/simple_httpclient_test.py

index 37b0bc27fdd453da15d2a8d11c8a5a1eb5f7e726..ac6bd26e5ea4c42411bde15cfb62a85d4e8a7cec 100644 (file)
@@ -126,7 +126,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
             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)
@@ -167,11 +167,20 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
                 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]
@@ -229,7 +238,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
             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,
@@ -284,10 +293,17 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
             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:
@@ -307,7 +323,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
         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)
index da11abbe37c3c1895be7d495381ad38767340679..ca1e2869b7adf6664c25a28ac2a7ae13f1f95565 100644 (file)
@@ -10,6 +10,7 @@ import re
 import socket
 import ssl
 import sys
+import time
 
 from tornado.escape import to_unicode
 from tornado import gen
@@ -17,8 +18,8 @@ from tornado.httpclient import AsyncHTTPClient
 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
@@ -237,6 +238,99 @@ class SimpleHTTPClientTestMixin(object):
             # 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
@@ -249,7 +343,7 @@ class SimpleHTTPClientTestMixin(object):
         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()()
 
@@ -357,7 +451,7 @@ class SimpleHTTPClientTestMixin(object):
 
             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()