diff --git a/tornado/iostream.py b/tornado/iostream.py index 9e95a066..df1f27f6 100644 --- a/tornado/iostream.py +++ b/tornado/iostream.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, division, with_statement import collections import errno import logging +import os import socket import sys import re @@ -48,6 +49,9 @@ class IOStream(object): and may either be connected before passing it to the IOStream or connected with IOStream.connect. + When a stream is closed due to an error, the IOStream's `error` + attribute contains the exception object. + A very simple (and broken) HTTP client using this class:: from tornado import ioloop @@ -84,6 +88,7 @@ class IOStream(object): self.io_loop = io_loop or ioloop.IOLoop.instance() self.max_buffer_size = max_buffer_size self.read_chunk_size = read_chunk_size + self.error = None self._read_buffer = collections.deque() self._write_buffer = collections.deque() self._read_buffer_size = 0 @@ -214,6 +219,8 @@ class IOStream(object): def close(self): """Close this stream.""" if self.socket is not None: + if any(sys.exc_info()): + self.error = sys.exc_info()[1] if self._read_until_close: callback = self._read_callback self._read_callback = None @@ -264,6 +271,9 @@ class IOStream(object): if not self.socket: return if events & self.io_loop.ERROR: + errno = self.socket.getsockopt(socket.SOL_SOCKET, + socket.SO_ERROR) + self.error = socket.error(errno, os.strerror(errno)) # We may have queued up a user callback in _handle_read or # _handle_write, so don't close the IOStream until those # callbacks have had a chance to run. diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py index ac8c207a..6340eda9 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -5,6 +5,7 @@ from tornado.iostream import IOStream from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase, get_unused_port from tornado.util import b from tornado.web import RequestHandler, Application +import errno import socket import time @@ -91,6 +92,16 @@ class TestIOStream(AsyncHTTPTestCase, LogTrapTestCase): stream.connect(("localhost", port), connect_callback) self.wait() self.assertFalse(self.connect_called) + self.assertTrue(isinstance(stream.error, socket.error), stream.error) + self.assertEqual(stream.error.args[0], errno.ECONNREFUSED) + + def test_gaierror(self): + # Test that IOStream sets its exc_info on getaddrinfo error + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + stream = IOStream(s, io_loop=self.io_loop) + stream.set_close_callback(self.stop) + stream.connect(('adomainthatdoesntexist.asdf', 54321)) + self.assertTrue(isinstance(stream.error, socket.gaierror), stream.error) def test_connection_closed(self): # When a server sends a response and then closes the connection,