Repository: falconry/falcon

Issue:
Middleware post process error handling
When middleware is added to the api and we have a post_process on it, any exceptions thrown in the responder are not aggregated to an appropriate response. Instead, they are ignored and the post_process attempts to run on as though nothing happened.

It does not matter whether an exception handler is added to the api or not.

Let me know if I just misunderstood how to handle these



Discussion:
Hi @esmalifesum, thanks for bringing up this issue. You are correct in that the framework does not skip `process_response` methods when an exception is raised. This is done in case those methods need to perform critical post-processing steps.  

When an instance of `falcon.HTTPError` is raised in a responder, the framework should render that into a response, while also executing `process_response`. Similarly, when other types of exception are raised, custom error handlers are given the chance to render that into a response. If you are seeing a situation in which the errors are not rendering into a response, that's definitely a bug.

Also, while looking at the code, I found a a case in which the exception is rendered to a response only after passing through the middleware components. Conceptually, as well as for the sake of consistency, I think it would make more sense to always render the response from the exception first.

Is there a particular type of middleware you hand in mind that should be short-circuited when an error is raised? If that functionality is useful, we can brainstorm some ways to support it.

Hi Kurt,

We have an exception handler currently wrapped around the responders. If
any of these raise an exception, we will handle the exception as our app
needs within the handler. If this happens, I would expect for my post
processors not to even be called as they are there to process only
valid/sane responses from the responders.

Are you saying that each middleware's post process method should have an
exception handler on its own as well?

Maybe I'm not thinking around this the right way and there's a better
pattern for it. If so, can you provide a more thorough scenario/pattern for
us to follow?

Esma

On Thu, Sep 17, 2015 at 5:31 AM, Kurt Griffiths notifications@github.com
wrote:

> Hi @esmalifesum https://github.com/esmalifesum, thanks for bringing up
> this issue. You are correct in that the framework does not skip
> process_response methods when an exception is raised. This is done in
> case those methods need to perform critical post-processing steps.
> 
> When an instance of falcon.HTTPError is raised in a responder, the
> framework should render that into a response, while also executing
> process_response. Similarly, when other types of exception are raised,
> custom error handlers are given the chance to render that into a response.
> If you are seeing a situation in which the errors are not rendering into a
> response, that's definitely a bug.
> 
> Also, while looking at the code, I found a a case in which the exception
> is rendered to a response only after passing through the middleware
> components. Conceptually, as well as for the sake of consistency, I think
> it would make more sense to always render the response from the exception
> first.
> 
> Is there a particular type of middleware you hand in mind that should be
> short-circuited when an error is raised? If that functionality is useful,
> we can brainstorm some ways to support it.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/falconry/falcon/issues/606#issuecomment-140959511.

There are times when you may want to perform post-processing regardless of whether an error was raised. But I can see your point about a need to be more selective in some cases.

One way we could support this would be by adding an additional middleware method, `process_successful_response()`. Thoughts?

Yes, I think something like that would allow us more flexibility and let us avoid coding nonsense into middleware just to handle exceptions when there is already an exceptions handler. My team would definitely use your suggested method.

Is there any progress regarding this issue? 

I need to validate responses agains swagger schema. 
When a falcon HTTP exception is raised in a responder or in process_resource middleware method (which raises HTTPBadRequest when request body is invalid), the status code of the response is still 200 when it comes to process_response. 

At which point does falcon set appropriate response status and body when an HTTP error is raised? 
How can I process only successful responses? 
How can I identify that a response is not successful in a process_response middleware method?

I've created a stack overflow question where you can find a minimal example: http://stackoverflow.com/questions/38079209/error-handling-in-falcon-middleware


Resolving patch:
diff --git a/falcon/api.py b/falcon/api.py
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -73,7 +73,7 @@ def process_resource(self, req, resp, resource, params):
                                 arguments.
                         \"\"\"
 
-                    def process_response(self, req, resp, resource)
+                    def process_response(self, req, resp, resource, req_succeeded)
                         \"\"\"Post-processing of the response (after routing).
 
                         Args:
@@ -82,6 +82,9 @@ def process_response(self, req, resp, resource)
                             resource: Resource object to which the request was
                                 routed. May be None if no route was found
                                 for the request.
+                            req_succeeded: True if no exceptions were raised
+                                while the framework processed and routed the
+                                request; otherwise False.
                         \"\"\"
 
             See also :ref:`Middleware <middleware>`.
@@ -161,9 +164,11 @@ def __call__(self, env, start_response):  # noqa: C901
         req = self._request_type(env, options=self.req_options)
         resp = self._response_type()
         resource = None
-        mw_pr_stack = []  # Keep track of executed middleware components
         params = {}
 
+        mw_pr_stack = []  # Keep track of executed middleware components
+        req_succeeded = False
+
         try:
             try:
                 # NOTE(ealogar): The execution of request middleware
@@ -202,6 +207,7 @@ def __call__(self, env, start_response):  # noqa: C901
                                 process_resource(req, resp, resource, params)
 
                     responder(req, resp, **params)
+                    req_succeeded = True
                 except Exception as ex:
                     if not self._handle_exception(ex, req, resp, params):
                         raise
@@ -217,11 +223,13 @@ def __call__(self, env, start_response):  # noqa: C901
             while mw_pr_stack:
                 process_response = mw_pr_stack.pop()
                 try:
-                    process_response(req, resp, resource)
+                    process_response(req, resp, resource, req_succeeded)
                 except Exception as ex:
                     if not self._handle_exception(ex, req, resp, params):
                         raise
 
+                    req_succeeded = False
+
         #
         # Set status and headers
         #
diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py
--- a/falcon/api_helpers.py
+++ b/falcon/api_helpers.py
@@ -14,6 +14,9 @@
 
 """Utilities for the API class."""
 
+from functools import wraps
+import inspect
+
 from falcon import util
 
 
@@ -49,6 +52,21 @@ def prepare_middleware(middleware=None):
             msg = '{0} does not implement the middleware interface'
             raise TypeError(msg.format(component))
 
+        if process_response:
+            # NOTE(kgriffs): Shim older implementations to ensure
+            # backwards-compatibility.
+            spec = inspect.getargspec(process_response)
+
+            if len(spec.args) == 4:  # (self, req, resp, resource)
+                def let(process_response=process_response):
+                    @wraps(process_response)
+                    def shim(req, resp, resource, req_succeeded):
+                        process_response(req, resp, resource)
+
+                    return shim
+
+                process_response = let()
+
         prepared_middleware.append((process_request, process_resource,
                                     process_response))
 


Test patch:
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -11,8 +11,11 @@
 
 class CaptureResponseMiddleware(object):
 
-    def process_response(self, req, resp, resource):
+    def process_response(self, req, resp, resource, req_succeeded):
+        self.req = req
         self.resp = resp
+        self.resource = resource
+        self.req_succeeded = req_succeeded
 
 
 class RequestTimeMiddleware(object):
@@ -25,9 +28,10 @@ def process_resource(self, req, resp, resource, params):
         global context
         context['mid_time'] = datetime.utcnow()
 
-    def process_response(self, req, resp, resource):
+    def process_response(self, req, resp, resource, req_succeeded):
         global context
         context['end_time'] = datetime.utcnow()
+        context['req_succeeded'] = req_succeeded
 
 
 class TransactionIdMiddleware(object):
@@ -36,6 +40,9 @@ def process_request(self, req, resp):
         global context
         context['transaction_id'] = 'unique-req-id'
 
+    def process_response(self, req, resp, resource):
+        pass
+
 
 class ExecutedFirstMiddleware(object):
 
@@ -49,11 +56,18 @@ def process_resource(self, req, resp, resource, params):
         context['executed_methods'].append(
             '{0}.{1}'.format(self.__class__.__name__, 'process_resource'))
 
+    # NOTE(kgriffs): This also tests that the framework can continue to
+    # call process_response() methods that do not have a 'req_succeeded'
+    # arg.
     def process_response(self, req, resp, resource):
         global context
         context['executed_methods'].append(
             '{0}.{1}'.format(self.__class__.__name__, 'process_response'))
 
+        context['req'] = req
+        context['resp'] = resp
+        context['resource'] = resource
+
 
 class ExecutedLastMiddleware(ExecutedFirstMiddleware):
     pass
@@ -110,6 +124,7 @@ def test_skip_process_resource(self):
         self.assertIn('start_time', context)
         self.assertNotIn('mid_time', context)
         self.assertIn('end_time', context)
+        self.assertFalse(context['req_succeeded'])
 
     def test_add_invalid_middleware(self):
         """Test than an invalid class can not be added as middleware"""
@@ -147,6 +162,7 @@ def test_log_get_request(self):
         body = self.simulate_json_request(self.test_route)
         self.assertEqual(_EXPECTED_BODY, body)
         self.assertEqual(self.srmock.status, falcon.HTTP_200)
+
         self.assertIn('start_time', context)
         self.assertIn('mid_time', context)
         self.assertIn('end_time', context)
@@ -155,6 +171,8 @@ def test_log_get_request(self):
         self.assertTrue(context['end_time'] >= context['start_time'],
                         'process_response not executed after request')
 
+        self.assertTrue(context['req_succeeded'])
+
 
 class TestTransactionIdMiddleware(TestMiddleware):
 
@@ -194,6 +212,16 @@ def test_generate_trans_id_and_time_with_request(self):
         self.assertTrue(context['end_time'] >= context['start_time'],
                         'process_response not executed after request')
 
+    def test_legacy_middleware_called_with_correct_args(self):
+        global context
+        self.api = falcon.API(middleware=[ExecutedFirstMiddleware()])
+        self.api.add_route(self.test_route, MiddlewareClassResource())
+
+        self.simulate_request(self.test_route)
+        self.assertIsInstance(context['req'], falcon.Request)
+        self.assertIsInstance(context['resp'], falcon.Response)
+        self.assertIsInstance(context['resource'], MiddlewareClassResource)
+
     def test_middleware_execution_order(self):
         global context
         self.api = falcon.API(middleware=[ExecutedFirstMiddleware(),
@@ -220,6 +248,8 @@ def test_multiple_reponse_mw_throw_exception(self):
         """Test that error in inner middleware leaves"""
         global context
 
+        context['req_succeeded'] = []
+
         class RaiseStatusMiddleware(object):
             def process_response(self, req, resp, resource):
                 raise falcon.HTTPStatus(falcon.HTTP_201)
@@ -229,10 +259,12 @@ def process_response(self, req, resp, resource):
                 raise falcon.HTTPError(falcon.HTTP_748)
 
         class ProcessResponseMiddleware(object):
-            def process_response(self, req, resp, resource):
+            def process_response(self, req, resp, resource, req_succeeded):
                 context['executed_methods'].append('process_response')
+                context['req_succeeded'].append(req_succeeded)
 
-        self.api = falcon.API(middleware=[RaiseErrorMiddleware(),
+        self.api = falcon.API(middleware=[ProcessResponseMiddleware(),
+                                          RaiseErrorMiddleware(),
                                           ProcessResponseMiddleware(),
                                           RaiseStatusMiddleware(),
                                           ProcessResponseMiddleware()])
@@ -242,8 +274,9 @@ def process_response(self, req, resp, resource):
 
         self.assertEqual(self.srmock.status, falcon.HTTP_748)
 
-        expected_methods = ['process_response', 'process_response']
+        expected_methods = ['process_response'] * 3
         self.assertEqual(context['executed_methods'], expected_methods)
+        self.assertEqual(context['req_succeeded'], [True, False, False])
 
     def test_inner_mw_throw_exception(self):
         """Test that error in inner middleware leaves"""
@@ -475,6 +508,13 @@ def test_error_composed_before_resp_middleware_called(self):
         composed_body = json.loads(self.mw.resp.body)
         self.assertEqual(composed_body['title'], self.srmock.status)
 
+        self.assertFalse(self.mw.req_succeeded)
+
+        # NOTE(kgriffs): Sanity-check the other params passed to
+        # process_response()
+        self.assertIsInstance(self.mw.req, falcon.Request)
+        self.assertIsInstance(self.mw.resource, MiddlewareClassResource)
+
     def test_http_status_raised_from_error_handler(self):
         def _http_error_handler(error, req, resp, params):
             raise falcon.HTTPStatus(falcon.HTTP_201)