Repository: python-odin/odinweb

Issue:
Added error hook for middleware
Allow middleware to catch errors.
Returning an explicit `True` indicates that the exception has been handled

Comments:

Patch:
diff --git a/odinweb/containers.py b/odinweb/containers.py
index 43622c4..975da81 100644
--- a/odinweb/containers.py
+++ b/odinweb/containers.py
@@ -274,6 +274,17 @@ class ApiInterfaceBase(ApiContainer):
         """
         Handle an *un-handled* exception.
         """
+        # Let middleware attempt to handle exception
+        try:
+            for middleware in self.middleware.handle_500:
+                resource = middleware(request, exception)
+                if resource:
+                    return resource
+
+        except Exception as ex:  # noqa - This is a top level handler
+            exception = ex
+
+        # Fallback to generic error
         logger.exception('Internal Server Error: %s', exception, extra={
             'status_code': 500,
             'request': request
@@ -343,7 +354,12 @@ class ApiInterfaceBase(ApiContainer):
                 # error processing, this often provides convenience features
                 # to aid in the debugging process.
                 raise
-            resource = self.handle_500(request, e)
+
+            resource = None
+            # Fallback to the default handler
+            if resource is None:
+                resource = self.handle_500(request, e)
+
             status = resource.status
 
         if isinstance(status, HTTPStatus):
diff --git a/odinweb/data_structures.py b/odinweb/data_structures.py
index cd79032..3ec6db0 100644
--- a/odinweb/data_structures.py
+++ b/odinweb/data_structures.py
@@ -439,6 +439,14 @@ class MiddlewareList(list):
         middleware = sort_by_priority(self, reverse=True)
         return tuple(m.post_dispatch for m in middleware if hasattr(m, 'post_dispatch'))
 
+    @lazy_property
+    def handle_500(self):
+        """
+        List of handle-error methods from registered middleware.
+        """
+        middleware = sort_by_priority(self, reverse=True)
+        return tuple(m.handle_500 for m in middleware if hasattr(m, 'handle_500'))
+
     @lazy_property
     def post_swagger(self):
         """


Test patch:
diff --git a/tests/test_containers.py b/tests/test_containers.py
index 475acd4..0be6c61 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 
 import pytest
+from odinweb.resources import Error
 
 from odin.exceptions import ValidationError
 from odinweb import api
@@ -302,6 +303,37 @@ class TestApiInterfaceBase(object):
         with pytest.raises(ValueError):
             target.dispatch(operation, MockRequest())
 
+    def test_dispatch__error_handled_by_middleware(self):
+        class ErrorMiddleware(object):
+            def handle_500(self, request, exception):
+                assert isinstance(exception, ValueError)
+                return Error.from_status(HTTPStatus.SEE_OTHER, 0,
+                                         "Quick over there...")
+
+        def callback(request):
+            raise ValueError()
+
+        target = containers.ApiInterfaceBase(middleware=[ErrorMiddleware()])
+        operation = Operation(callback)
+
+        actual = target.dispatch(operation, MockRequest())
+        assert actual.status == 303
+
+    def test_dispatch__error_handled_by_middleware_raises_exception(self):
+        class ErrorMiddleware(object):
+            def handle_500(self, request, exception):
+                assert isinstance(exception, ValueError)
+                raise ValueError
+
+        def callback(request):
+            raise ValueError()
+
+        target = containers.ApiInterfaceBase(middleware=[ErrorMiddleware()])
+        operation = Operation(callback)
+
+        actual = target.dispatch(operation, MockRequest())
+        assert actual.status == 500
+
     def test_dispatch__encode_error_with_debug_enabled(self):
         def callback(request):
             raise api.ImmediateHttpResponse(ValueError, HTTPStatus.NOT_MODIFIED, {})