Note that the close callback is scheduled to run *after* other
callbacks on the stream (to allow for buffered data to be processed),
so you may see this error before you see the close callback.
+
+ The ``real_error`` attribute contains the underlying error that caused
+ the stream to close (if any).
+
+ .. versionchanged:: 4.3
+ Added the ``real_error`` attribute.
"""
- pass
+ def __init__(self, real_error=None):
+ super(StreamClosedError, self).__init__('Stream is closed')
+ self.real_error = real_error
class UnsatisfiableReadError(Exception):
futures.append(self._ssl_connect_future)
self._ssl_connect_future = None
for future in futures:
- if self._is_connreset(self.error):
- # Treat connection resets as closed connections so
- # clients only have to catch one kind of exception
- # to avoid logging.
- future.set_exception(StreamClosedError())
- else:
- future.set_exception(self.error or StreamClosedError())
+ future.set_exception(StreamClosedError(real_error=self.error))
if self._close_callback is not None:
cb = self._close_callback
self._close_callback = None
pos = self._read_to_buffer_loop()
except UnsatisfiableReadError:
raise
- except Exception:
- gen_log.warning("error on read", exc_info=True)
+ except Exception as e:
+ gen_log.warning("error on read: %s" % e)
self.close(exc_info=True)
return
if pos is not None:
def _check_closed(self):
if self.closed():
- raise StreamClosedError("Stream is closed")
+ raise StreamClosedError(real_error=self.error)
def _maybe_add_error_listener(self):
# This method is part of an optimization: to detect a connection that
def close_callback():
if not future.done():
+ # Note that unlike most Futures returned by IOStream,
+ # this one passes the underlying error through directly
+ # instead of wrapping everything in a StreamClosedError
+ # with a real_error attribute. This is because once the
+ # connection is established it's more helpful to raise
+ # the SSLError directly than to hide it behind a
+ # StreamClosedError (and the client is expecting SSL
+ # issues rather than network issues since this method is
+ # named start_tls).
future.set_exception(ssl_stream.error or StreamClosedError())
if orig_close_callback is not None:
orig_close_callback()
return False
try:
ssl_match_hostname(peercert, self._server_hostname)
- except SSLCertificateError:
- gen_log.warning("Invalid SSL certificate", exc_info=True)
+ except SSLCertificateError as e:
+ gen_log.warning("Invalid SSL certificate: %s" % e)
return False
else:
return True
if self.final_callback:
self._remove_timeout()
if isinstance(value, StreamClosedError):
- value = HTTPError(599, "Stream closed")
+ if value.real_error is None:
+ value = HTTPError(599, "Stream closed")
+ else:
+ value = value.real_error
self._run_callback(HTTPResponse(self.request, 599, error=value,
request_time=self.io_loop.time() - self.start_time,
))
response = self.wait()
self.assertEqual(response.code, 599)
+ def test_error_logging(self):
+ # No stack traces are logged for SSL errors.
+ with ExpectLog(gen_log, 'SSL Error') as expect_log:
+ self.http_client.fetch(
+ self.get_url("/").replace("https:", "http:"),
+ self.stop)
+ response = self.wait()
+ self.assertEqual(response.code, 599)
+ self.assertFalse(expect_log.logged_stack)
+
# Python's SSL implementation differs significantly between versions.
# For example, SSLv3 and TLSv1 throw an exception if you try to read
# from the socket before the handshake is complete, but the default
with ExpectLog(gen_log, "SSL Error"):
with self.assertRaises(ssl.SSLError):
yield client_future
- with self.assertRaises((ssl.SSLError, socket.error)):
+ with self.assertRaises(ssl.SSLError):
yield server_future
resp = self.fetch("/hello", ssl_options=ctx)
self.assertRaises(ssl.SSLError, resp.rethrow)
+ def test_error_logging(self):
+ # No stack traces are logged for SSL errors (in this case,
+ # failure to validate the testing self-signed cert).
+ # The SSLError is exposed through ssl.SSLError.
+ with ExpectLog(gen_log, '.*') as expect_log:
+ response = self.fetch("/", validate_cert=True)
+ self.assertEqual(response.code, 599)
+ self.assertIsInstance(response.error, ssl.SSLError)
+ self.assertFalse(expect_log.logged_stack)
+
class CreateAsyncHTTPClientTestCase(AsyncTestCase):
def setUp(self):
Useful to make tests of error conditions less noisy, while still
leaving unexpected log entries visible. *Not thread safe.*
+ The attribute ``logged_stack`` is set to true if any exception
+ stack trace was logged.
+
Usage::
with ExpectLog('tornado.application', "Uncaught exception"):
error_response = self.fetch("/some_page")
+
+ .. versionchanged:: 4.3
+ Added the ``logged_stack`` attribute.
"""
def __init__(self, logger, regex, required=True):
"""Constructs an ExpectLog context manager.
self.regex = re.compile(regex)
self.required = required
self.matched = False
+ self.logged_stack = False
def filter(self, record):
+ if record.exc_info:
+ self.logged_stack = True
message = record.getMessage()
if self.regex.match(message):
self.matched = True
def __enter__(self):
self.logger.addFilter(self)
+ return self
def __exit__(self, typ, value, tb):
self.logger.removeFilter(self)