From: Color Fuzzy Date: Sat, 19 Dec 2015 19:34:07 +0000 (+0800) Subject: add more detailed information to HTTP Timeout error raised by SimpleAsyncHTTPClient X-Git-Tag: v4.4.0b1~59^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9a911b09965145af15bb5bbac4ed883f31166fab;p=thirdparty%2Ftornado.git add more detailed information to HTTP Timeout error raised by SimpleAsyncHTTPClient --- diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index 37b0bc27f..ac6bd26e5 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -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) diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index da11abbe3..ca1e2869b 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -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()