Skip to content

Commit

Permalink
Avoid setting IOStream.error to wrong exception, close #651
Browse files Browse the repository at this point in the history
  • Loading branch information
ajdavis committed Dec 11, 2012
1 parent 440e535 commit 24c3d41
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 15 deletions.
32 changes: 18 additions & 14 deletions tornado/iostream.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -209,10 +209,14 @@ def set_close_callback(self, callback):
"""Call the given callback when the stream is closed.""" """Call the given callback when the stream is closed."""
self._close_callback = stack_context.wrap(callback) self._close_callback = stack_context.wrap(callback)


def close(self): def close(self, exc_info=False):
"""Close this stream.""" """Close this stream.
If `exc_info` is true, set the `error` attribute to the current
exception from ``sys.exc_info()``.
"""
if not self.closed(): if not self.closed():
if any(sys.exc_info()): if exc_info and any(sys.exc_info()):
self.error = sys.exc_info()[1] self.error = sys.exc_info()[1]
if self._read_until_close: if self._read_until_close:
callback = self._read_callback callback = self._read_callback
Expand Down Expand Up @@ -285,7 +289,7 @@ def _handle_events(self, fd, events):
except Exception: except Exception:
gen_log.error("Uncaught exception, closing connection.", gen_log.error("Uncaught exception, closing connection.",
exc_info=True) exc_info=True)
self.close() self.close(exc_info=True)
raise raise


def _run_callback(self, callback, *args): def _run_callback(self, callback, *args):
Expand All @@ -300,7 +304,7 @@ def wrapper():
# (It would eventually get closed when the socket object is # (It would eventually get closed when the socket object is
# gc'd, but we don't want to rely on gc happening before we # gc'd, but we don't want to rely on gc happening before we
# run out of file descriptors) # run out of file descriptors)
self.close() self.close(exc_info=True)
# Re-raise the exception so that IOLoop.handle_callback_exception # Re-raise the exception so that IOLoop.handle_callback_exception
# can see it and log the error # can see it and log the error
raise raise
Expand Down Expand Up @@ -348,7 +352,7 @@ def _handle_read(self):
self._pending_callbacks -= 1 self._pending_callbacks -= 1
except Exception: except Exception:
gen_log.warning("error on read", exc_info=True) gen_log.warning("error on read", exc_info=True)
self.close() self.close(exc_info=True)
return return
if self._read_from_buffer(): if self._read_from_buffer():
return return
Expand Down Expand Up @@ -397,9 +401,9 @@ def _read_to_buffer(self):
# Treat ECONNRESET as a connection close rather than # Treat ECONNRESET as a connection close rather than
# an error to minimize log spam (the exception will # an error to minimize log spam (the exception will
# be available on self.error for apps that care). # be available on self.error for apps that care).
self.close() self.close(exc_info=True)
return return
self.close() self.close(exc_info=True)
raise raise
if chunk is None: if chunk is None:
return 0 return 0
Expand Down Expand Up @@ -503,7 +507,7 @@ def _handle_write(self):
else: else:
gen_log.warning("Write error on %d: %s", gen_log.warning("Write error on %d: %s",
self.fileno(), e) self.fileno(), e)
self.close() self.close(exc_info=True)
return return
if not self._write_buffer and self._write_callback: if not self._write_buffer and self._write_callback:
callback = self._write_callback callback = self._write_callback
Expand Down Expand Up @@ -664,7 +668,7 @@ def connect(self, address, callback=None):
if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK): if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK):
gen_log.warning("Connect error on fd %d: %s", gen_log.warning("Connect error on fd %d: %s",
self.socket.fileno(), e) self.socket.fileno(), e)
self.close() self.close(exc_info=True)
return return
self._connect_callback = stack_context.wrap(callback) self._connect_callback = stack_context.wrap(callback)
self._add_io_state(self.io_loop.WRITE) self._add_io_state(self.io_loop.WRITE)
Expand Down Expand Up @@ -733,19 +737,19 @@ def _do_ssl_handshake(self):
return return
elif err.args[0] in (ssl.SSL_ERROR_EOF, elif err.args[0] in (ssl.SSL_ERROR_EOF,
ssl.SSL_ERROR_ZERO_RETURN): ssl.SSL_ERROR_ZERO_RETURN):
return self.close() return self.close(exc_info=True)
elif err.args[0] == ssl.SSL_ERROR_SSL: elif err.args[0] == ssl.SSL_ERROR_SSL:
try: try:
peer = self.socket.getpeername() peer = self.socket.getpeername()
except: except:
peer = '(not connected)' peer = '(not connected)'
gen_log.warning("SSL Error on %d %s: %s", gen_log.warning("SSL Error on %d %s: %s",
self.socket.fileno(), peer, err) self.socket.fileno(), peer, err)
return self.close() return self.close(exc_info=True)
raise raise
except socket.error, err: except socket.error, err:
if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET): if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET):
return self.close() return self.close(exc_info=True)
else: else:
self._ssl_accepting = False self._ssl_accepting = False
if self._ssl_connect_callback is not None: if self._ssl_connect_callback is not None:
Expand Down Expand Up @@ -842,7 +846,7 @@ def read_from_fd(self):
elif e.args[0] == errno.EBADF: elif e.args[0] == errno.EBADF:
# If the writing half of a pipe is closed, select will # If the writing half of a pipe is closed, select will
# report it as readable but reads will fail with EBADF. # report it as readable but reads will fail with EBADF.
self.close() self.close(exc_info=True)
return None return None
else: else:
raise raise
Expand Down
21 changes: 20 additions & 1 deletion tornado/test/iostream_test.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from tornado import netutil from tornado import netutil
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream, PipeIOStream from tornado.iostream import IOStream, SSLIOStream, PipeIOStream
from tornado.log import gen_log from tornado.log import gen_log, app_log
from tornado.stack_context import NullContext
from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, bind_unused_port, ExpectLog from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, bind_unused_port, ExpectLog
from tornado.test.util import unittest, skipIfNonUnix from tornado.test.util import unittest, skipIfNonUnix
from tornado.util import b from tornado.util import b
Expand Down Expand Up @@ -197,6 +198,24 @@ def test_gaierror(self):
stream.connect(('an invalid domain', 54321)) stream.connect(('an invalid domain', 54321))
self.assertTrue(isinstance(stream.error, socket.gaierror), stream.error) self.assertTrue(isinstance(stream.error, socket.gaierror), stream.error)


def test_read_callback_error(self):
# Test that IOStream sets its exc_info when a read callback throws
server, client = self.make_iostream_pair()
try:
server.set_close_callback(self.stop)
with ExpectLog(
app_log, "(Uncaught exception|Exception in callback)"
):
# Clear ExceptionStackContext so IOStream catches error
with NullContext():
server.read_bytes(1, callback=lambda data: 1 / 0)
client.write(b("1"))
self.wait()
self.assertTrue(isinstance(server.error, ZeroDivisionError))
finally:
server.close()
client.close()

def test_streaming_callback(self): def test_streaming_callback(self):
server, client = self.make_iostream_pair() server, client = self.make_iostream_pair()
try: try:
Expand Down

0 comments on commit 24c3d41

Please sign in to comment.