diff --git a/tornado/httpclient.py b/tornado/httpclient.py index c2e68623..9179227b 100644 --- a/tornado/httpclient.py +++ b/tornado/httpclient.py @@ -603,9 +603,12 @@ class HTTPError(Exception): """ def __init__(self, code, message=None, response=None): self.code = code - message = message or httputil.responses.get(code, "Unknown") + self.message = message or httputil.responses.get(code, "Unknown") self.response = response - Exception.__init__(self, "HTTP %d: %s" % (self.code, message)) + super(HTTPError, self).__init__(code, message, response) + + def __str__(self): + return "HTTP %d: %s" % (self.code, self.message) class _RequestProxy(object): diff --git a/tornado/ioloop.py b/tornado/ioloop.py index a49e3ca9..195475ba 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -914,7 +914,7 @@ class PollIOLoop(IOLoop): # with other threads, or waking logic will induce a race. with self._callback_lock: if self._closing: - raise RuntimeError("IOLoop is closing") + return list_empty = not self._callbacks self._callbacks.append(functools.partial( stack_context.wrap(callback), *args, **kwargs)) @@ -927,7 +927,7 @@ class PollIOLoop(IOLoop): self._waker.wake() else: if self._closing: - raise RuntimeError("IOLoop is closing") + return # If we're on the IOLoop's thread, we don't need the lock, # since we don't need to wake anyone, just add the # callback. Blindly insert into self._callbacks. This is diff --git a/tornado/iostream.py b/tornado/iostream.py index 1e677104..0bad0acd 100644 --- a/tornado/iostream.py +++ b/tornado/iostream.py @@ -725,18 +725,22 @@ class BaseIOStream(object): to read (i.e. the read returns EWOULDBLOCK or equivalent). On error closes the socket and raises an exception. """ - try: - chunk = self.read_from_fd() - except (socket.error, IOError, OSError) as e: - # ssl.SSLError is a subclass of socket.error - if self._is_connreset(e): - # Treat ECONNRESET as a connection close rather than - # an error to minimize log spam (the exception will - # be available on self.error for apps that care). + while True: + try: + chunk = self.read_from_fd() + except (socket.error, IOError, OSError) as e: + if errno_from_exception(e) == errno.EINTR: + continue + # ssl.SSLError is a subclass of socket.error + if self._is_connreset(e): + # Treat ECONNRESET as a connection close rather than + # an error to minimize log spam (the exception will + # be available on self.error for apps that care). + self.close(exc_info=True) + return self.close(exc_info=True) - return - self.close(exc_info=True) - raise + raise + break if chunk is None: return 0 self._read_buffer.append(chunk) diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py index 0d6e6eed..94d045de 100644 --- a/tornado/test/httpclient_test.py +++ b/tornado/test/httpclient_test.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, with_statement import base64 import binascii from contextlib import closing +import copy import functools import sys import threading @@ -629,3 +630,15 @@ class HTTPRequestTestCase(unittest.TestCase): request = HTTPRequest('http://example.com', if_modified_since=http_date) self.assertEqual(request.headers, {'If-Modified-Since': format_timestamp(http_date)}) + + +class HTTPErrorTestCase(unittest.TestCase): + def test_copy(self): + e = HTTPError(403) + e2 = copy.copy(e) + self.assertIsNot(e, e2) + self.assertEqual(e.code, e2.code) + + def test_str(self): + e = HTTPError(403) + self.assertEqual(str(e), "HTTP 403: Forbidden") diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index 558c9ae8..3acbbfd9 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -15,6 +15,7 @@ from tornado.web import RequestHandler, authenticated, Application, asynchronous import binascii import contextlib +import copy import datetime import email.utils import itertools @@ -2544,15 +2545,19 @@ class FinishExceptionTest(SimpleHandlerTestCase): def get(self): self.set_status(401) self.set_header('WWW-Authenticate', 'Basic realm="something"') - self.write('authentication required') - raise Finish() + if self.get_argument('finish_value', ''): + raise Finish('authentication required') + else: + self.write('authentication required') + raise Finish() def test_finish_exception(self): - response = self.fetch('/') - self.assertEqual(response.code, 401) - self.assertEqual('Basic realm="something"', - response.headers.get('WWW-Authenticate')) - self.assertEqual(b'authentication required', response.body) + for url in ['/', '/?finish_value=1']: + response = self.fetch(url) + self.assertEqual(response.code, 401) + self.assertEqual('Basic realm="something"', + response.headers.get('WWW-Authenticate')) + self.assertEqual(b'authentication required', response.body) @wsgi_safe @@ -2671,3 +2676,12 @@ class RequestSummaryTest(SimpleHandlerTestCase): def test_missing_remote_ip(self): resp = self.fetch("/") self.assertEqual(resp.body, b"GET / (None)") + + +class HTTPErrorTest(unittest.TestCase): + def test_copy(self): + e = HTTPError(403, reason="Go away") + e2 = copy.copy(e) + self.assertIsNot(e, e2) + self.assertEqual(e.status_code, e2.status_code) + self.assertEqual(e.reason, e2.reason) diff --git a/tornado/test/websocket_test.py b/tornado/test/websocket_test.py index 23a4324c..7b47214d 100644 --- a/tornado/test/websocket_test.py +++ b/tornado/test/websocket_test.py @@ -130,7 +130,7 @@ class WebSocketTest(WebSocketBaseTestCase): @gen_test def test_websocket_gen(self): ws = yield self.ws_connect('/echo') - ws.write_message('hello') + yield ws.write_message('hello') response = yield ws.read_message() self.assertEqual(response, 'hello') yield self.close(ws) diff --git a/tornado/web.py b/tornado/web.py index dd0df5ef..f6ae767b 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -1473,7 +1473,7 @@ class RequestHandler(object): if isinstance(e, Finish): # Not an error; just finish the request without logging. if not self._finished: - self.finish() + self.finish(*e.args) return try: self.log_exception(*sys.exc_info()) @@ -2063,7 +2063,7 @@ class HTTPError(Exception): determined automatically from ``status_code``, but can be used to use a non-standard numeric code. """ - def __init__(self, status_code, log_message=None, *args, **kwargs): + def __init__(self, status_code=500, log_message=None, *args, **kwargs): self.status_code = status_code self.log_message = log_message self.args = args @@ -2084,10 +2084,14 @@ class HTTPError(Exception): class Finish(Exception): """An exception that ends the request without producing an error response. - When `Finish` is raised in a `RequestHandler`, the request will end - (calling `RequestHandler.finish` if it hasn't already been called), - but the outgoing response will not be modified and the error-handling - methods (including `RequestHandler.write_error`) will not be called. + When `Finish` is raised in a `RequestHandler`, the request will + end (calling `RequestHandler.finish` if it hasn't already been + called), but the error-handling methods (including + `RequestHandler.write_error`) will not be called. + + If `Finish()` was created with no arguments, the pending response + will be sent as-is. If `Finish()` was given an argument, that + argument will be passed to `RequestHandler.finish()`. This can be a more convenient way to implement custom error pages than overriding ``write_error`` (especially in library code):: @@ -2096,6 +2100,10 @@ class Finish(Exception): self.set_status(401) self.set_header('WWW-Authenticate', 'Basic realm="something"') raise Finish() + + .. versionchanged:: 4.3 + Arguments passed to ``Finish()`` will be passed on to + `RequestHandler.finish`. """ pass diff --git a/tornado/websocket.py b/tornado/websocket.py index 58262cc3..3b8b9a1f 100644 --- a/tornado/websocket.py +++ b/tornado/websocket.py @@ -208,12 +208,15 @@ class WebSocketHandler(tornado.web.RequestHandler): .. versionchanged:: 3.2 `WebSocketClosedError` was added (previously a closed connection would raise an `AttributeError`) + + .. versionchanged:: 4.3 + Returns a `.Future` which can be used for flow control. """ if self.ws_connection is None: raise WebSocketClosedError() if isinstance(message, dict): message = tornado.escape.json_encode(message) - self.ws_connection.write_message(message, binary=binary) + return self.ws_connection.write_message(message, binary=binary) def select_subprotocol(self, subprotocols): """Invoked when a new WebSocket requests specific subprotocols. @@ -671,7 +674,7 @@ class WebSocketProtocol13(WebSocketProtocol): frame += data self._wire_bytes_out += len(frame) try: - self.stream.write(frame) + return self.stream.write(frame) except StreamClosedError: self._abort() @@ -688,7 +691,7 @@ class WebSocketProtocol13(WebSocketProtocol): if self._compressor: message = self._compressor.compress(message) flags |= self.RSV1 - self._write_frame(True, opcode, message, flags=flags) + return self._write_frame(True, opcode, message, flags=flags) def write_ping(self, data): """Send ping frame.""" @@ -970,7 +973,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): def write_message(self, message, binary=False): """Sends a message to the WebSocket server.""" - self.protocol.write_message(message, binary) + return self.protocol.write_message(message, binary) def read_message(self, callback=None): """Reads a message from the WebSocket server.