diff --git a/docs/index.rst b/docs/index.rst index 43af1f6..0a21d58 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -265,19 +265,36 @@ subset of them: * ``after_event(*args)`` is called after the event handler with the event name, the namespace and the list of values the event handler returned. It may alter that values eventually. -* ``ignore_event`` is called before the middleware is applied to an +* ``handle_exception`` is called when an exception is raised during the + event handling process with the event name, namespace and the exception + object as its arguments. The exception could have been raised by either + the event handler itself, or the ``before_event`` and ``after_event`` + methods of another middleware that comes later in the middleware chain + (one that is applied more-specifically). +* ``ignore_for`` is called before the middleware is applied to an event handler with the event name and namespace as arguments. If its - return value resolves to ``True`` the middleware is not applied to that - particular event handler. - -If one of these methods returns something else than ``None``, execution -is stopped at that point and the returned value is treated as if it was -returned by the event handler. + return value resolves to ``True`` the middleware is ignored and not + applied to that particular event handler. Event handler middlewares can be chained. The ``before_event`` methods will be executed in the same order the middlewares were added, the ``after_event`` methods are called in reversed order. +If the ``before_event`` method returns something else than ``None``, +execution jumps to ``after_event`` of this middleware, passing the +arguments the method returned to it and continues the regular processing +from there by rippling up until the least specific middleware is reached. + +If an ``after_event`` method returns something else than ``None``, +execution is stopped at that point and the returned value is used as +the result of event handling. + +``handle_exception``, if present, must either handle the given exception +and return something that will be treated as the event handling result +and eventually be passed to the ``after_event`` method of less-specific +middlewares or re-raise the exception (or another one, +at their choice). + Note that you can also use classes (objects of type ``type``) instead of instances as event handler middlewares. If you do so, they are instantiated with no arguments every time they are used to process @@ -290,11 +307,14 @@ Example: from socketio import server class MyMiddleware(object): - def ignore_event(self, event, namespace): + def ignore_for(self, event, namespace): # This middleware won't be applied to connect and disconnect # event handlers. return event in ("connect", "disconnect") + def handle_exception(self, event, namespace, e): + return "Exception: %r" % e + def before_event(self, event, namespace, args): print("before_event called with:", args) if len(args) < 2 or args[1] != "secret": diff --git a/socketio/server.py b/socketio/server.py index 9ba5429..ef7d634 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -469,34 +469,80 @@ class Server(object): for middleware in middlewares: if isinstance(middleware, type): middleware = middleware() - if not hasattr(middleware, 'ignore_event') or \ - not middleware.ignore_event(event, namespace): + if not hasattr(middleware, 'ignore_for') or \ + not middleware.ignore_for(event, namespace): _middlewares.append(middleware) if not _middlewares: return handler def wrapped(*args): - args = list(args) - - for middleware in _middlewares: - if hasattr(middleware, 'before_event'): - result = middleware.before_event(event, namespace, args) - if result is not None: - return result - - result = handler(*args) - if result is None: - data = [] - elif isinstance(result, tuple): - data = list(result) - else: - data = [result] + def handle_exception(middlewares_processed, exc): + for middleware in \ + reversed(_middlewares[:middlewares_processed]): + if hasattr(middleware, 'handle_exception'): + try: + result = middleware.handle_exception( + event, namespace, exc) + # exception has been cought + return middlewares_processed, result + except Exception as e: + exc = e + middlewares_processed -= 1 + if exc is not None: + # Exception hasn't been cought by any middleware, + # hence we finally raise it. + raise e + + def reformat_result(result): + if result is None: + return [] + elif isinstance(result, tuple): + return list(result) + else: + return [result] - for middleware in reversed(_middlewares): - if hasattr(middleware, 'after_event'): - result = middleware.after_event(event, namespace, data) - if result is not None: - return result + args = list(args) + middlewares_processed = 0 + result = None + try: + # apply before_event methods + for middleware in _middlewares: + middlewares_processed += 1 + if hasattr(middleware, 'before_event'): + result = middleware.before_event( + event, namespace, args) + if result is not None: + break +# return result + if result is None: + # call the real event handler + result = handler(*args) + except Exception as exc: + middlewares_processed, result = \ + handle_exception(middlewares_processed, exc) + + data = reformat_result(result) + + while middlewares_processed > 0: + try: + # In case there was an exception handled, we ensure + # only the middlewares that are less-specific + # than the one that cought the exception get + # applied. More-specific middlewares should not be + # aware of less-specific ones and thus not process + # the output of them. + for middleware in \ + reversed(_middlewares[:middlewares_processed]): + if hasattr(middleware, 'after_event'): + result = middleware.after_event( + event, namespace, data) + if result is not None: + return result + middlewares_processed -= 1 + except Exception as exc: + middlewares_processed, result = \ + handle_exception(middlewares_processed, exc) + data = reformat_result(result) return tuple(data) diff --git a/tests/test_server.py b/tests/test_server.py index 8f14ac7..5c50138 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -50,7 +50,7 @@ class TestServer(unittest.TestCase): def test_middleware(self, eio): class MW: def __init__(self): - self.ignore_event = mock.MagicMock(side_effect=100 * [False]) + self.ignore_for = mock.MagicMock(side_effect=100 * [False]) self.before_event = mock.MagicMock(side_effect=100 * [None]) self.after_event = mock.MagicMock(side_effect=100 * [None]) @@ -58,7 +58,7 @@ class TestServer(unittest.TestCase): mw2 = MW() mw3 = MW() mw4 = MW() - mw4.ignore_event = mock.MagicMock(side_effect=[True] + 100 * [False]) + mw4.ignore_for = mock.MagicMock(side_effect=[True] + 100 * [False]) mw4.before_event = mock.MagicMock(side_effect=['x'] + 100 * [None]) mw4.after_event = mock.MagicMock(side_effect=['x'] + 100 * [None]) @@ -105,7 +105,7 @@ class TestServer(unittest.TestCase): self.assertEqual(mw4.before_event.call_count, 0) self.assertEqual(mw4.after_event.call_count, 0) - # again, this time mw4 before_event should be triggered + # again, this time mw4 before + after should be triggered as well s._trigger_event('foo bar', '/ns', '123') self.assertEqual(mw1.before_event.call_count, 3) self.assertEqual(mw1.after_event.call_count, 2) @@ -114,30 +114,29 @@ class TestServer(unittest.TestCase): self.assertEqual(mw3.before_event.call_count, 3) self.assertEqual(mw3.after_event.call_count, 2) self.assertEqual(mw4.before_event.call_count, 1) - self.assertEqual(mw4.after_event.call_count, 0) + self.assertEqual(mw4.after_event.call_count, 1) # again, this time mw4 before + after_event should be triggered - # but after_event should abort execution s._trigger_event('foo bar', '/ns', '123') self.assertEqual(mw1.before_event.call_count, 4) - self.assertEqual(mw1.after_event.call_count, 2) + self.assertEqual(mw1.after_event.call_count, 3) self.assertEqual(mw2.before_event.call_count, 0) self.assertEqual(mw2.after_event.call_count, 0) self.assertEqual(mw3.before_event.call_count, 4) - self.assertEqual(mw3.after_event.call_count, 2) + self.assertEqual(mw3.after_event.call_count, 3) self.assertEqual(mw4.before_event.call_count, 2) - self.assertEqual(mw4.after_event.call_count, 1) + self.assertEqual(mw4.after_event.call_count, 2) # only mw1 and mw2 should run completely s._trigger_event('abc', '/', '123') self.assertEqual(mw1.before_event.call_count, 5) - self.assertEqual(mw1.after_event.call_count, 3) + self.assertEqual(mw1.after_event.call_count, 4) self.assertEqual(mw2.before_event.call_count, 1) self.assertEqual(mw2.after_event.call_count, 1) self.assertEqual(mw3.before_event.call_count, 4) - self.assertEqual(mw3.after_event.call_count, 2) + self.assertEqual(mw3.after_event.call_count, 3) self.assertEqual(mw4.before_event.call_count, 2) - self.assertEqual(mw4.after_event.call_count, 1) + self.assertEqual(mw4.after_event.call_count, 2) def test_on_bad_event_name(self, eio): s = server.Server()