codecov-io: ## [Current coverage](https://codecov.io/gh/falconry/falcon/pull/890?src=pr) is 100% (diff: 100%)
> Merging [#890](https://codecov.io/gh/falconry/falcon/pull/890?src=pr) into [master](https://codecov.io/gh/falconry/falcon/branch/master?src=pr) will not change coverage

```diff
@@           master   #890   diff @@
====================================
  Files          30     30          
  Lines        1897   1910    +13   
  Methods         0      0          
  Messages        0      0          
  Branches      314    316     +2   
====================================
+ Hits         1897   1910    +13   
  Misses          0      0          
  Partials        0      0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [d9d1aed...bdae40c](https://codecov.io/gh/falconry/falcon/compare/d9d1aed01ef5e85d3164bd0e6b83f52293647c58...bdae40c242f2e4949a591973e09d7cf1a78033f8?src=pr)
jmvrbanac: LGTM :+1: 
jmvrbanac: :+1: 
diff --git a/docs/api/middleware.rst b/docs/api/middleware.rst
index f947b7f..b6e9036 100644
--- a/docs/api/middleware.rst
+++ b/docs/api/middleware.rst
@@ -47,7 +47,7 @@ Falcon's middleware interface is defined as follows:
                     method as keyword 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:
@@ -56,6 +56,9 @@ Falcon's middleware interface is defined as follows:
                 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.
             """
 
 .. Tip::
diff --git a/falcon/api.py b/falcon/api.py
index 853abb1..e1e985a 100644
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -73,7 +73,7 @@ class API(object):
                                 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 @@ class API(object):
                             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 @@ class API(object):
         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 @@ class API(object):
                                 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 @@ class API(object):
             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
index 9f91608..04a5979 100644
--- 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))
 

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
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 652306f..9d48c4c 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -11,8 +11,11 @@ context = {'executed_methods': []}
 
 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 @@ class RequestTimeMiddleware(object):
         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 @@ class TransactionIdMiddleware(object):
         global context
         context['transaction_id'] = 'unique-req-id'
 
+    def process_response(self, req, resp, resource):
+        pass
+
 
 class ExecutedFirstMiddleware(object):
 
@@ -49,11 +56,18 @@ class ExecutedFirstMiddleware(object):
         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 @@ class TestRequestTimeMiddleware(TestMiddleware):
         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 @@ class TestRequestTimeMiddleware(TestMiddleware):
         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 @@ class TestRequestTimeMiddleware(TestMiddleware):
         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 @@ class TestSeveralMiddlewares(TestMiddleware):
         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 @@ class TestSeveralMiddlewares(TestMiddleware):
         """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 @@ class TestSeveralMiddlewares(TestMiddleware):
                 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 @@ class TestSeveralMiddlewares(TestMiddleware):
 
         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 @@ class TestErrorHandling(TestMiddleware):
         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)

pip install -e .
parse_log_pytest
requirements.txt
pytest ddt pytest-randomly pytest-cov pytest-xdist
apt-get update apt-get install -y gcc
3.5
tools/test-requires
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
ddt==1.7.2
execnet==1.9.0
-e git+https://github.com/falconry/falcon.git@c2cb7091e28680671cb9092bf5f8a1840f6e6308#egg=falcon
fixtures==4.0.1
idna==3.10
importlib-metadata==4.8.3
iniconfig==1.1.1
packaging==21.3
pbr==6.1.1
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
pytest-cov==4.0.0
pytest-randomly==3.10.3
pytest-xdist==3.0.2
python-mimeparse==1.6.0
PyYAML==6.0.1
requests==2.27.1
six==1.17.0
testtools==2.6.0
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: falcon
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - ddt==1.7.2
      - execnet==1.9.0
      - fixtures==4.0.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - packaging==21.3
      - pbr==6.1.1
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - pytest-randomly==3.10.3
      - pytest-xdist==3.0.2
      - python-mimeparse==1.6.0
      - pyyaml==6.0.1
      - requests==2.27.1
      - six==1.17.0
      - testtools==2.6.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/falcon

tests/test_middleware.py::TestSeveralMiddlewares::test_multiple_reponse_mw_throw_exception tests/test_middleware.py::TestSeveralMiddlewares::test_inner_mw_throw_exception tests/test_middleware.py::TestSeveralMiddlewares::test_generate_trans_id_and_time_with_request tests/test_middleware.py::TestSeveralMiddlewares::test_inner_mw_with_ex_handler_throw_exception tests/test_middleware.py::TestRequestTimeMiddleware::test_skip_process_resource tests/test_middleware.py::TestRequestTimeMiddleware::test_log_get_request tests/test_middleware.py::TestErrorHandling::test_http_status_raised_from_error_handler tests/test_middleware.py::TestErrorHandling::test_error_composed_before_resp_middleware_called
tests/test_middleware.py::TestRemoveBasePathMiddleware::test_base_path_is_removed_before_routing tests/test_middleware.py::TestSeveralMiddlewares::test_middleware_execution_order tests/test_middleware.py::TestSeveralMiddlewares::test_legacy_middleware_called_with_correct_args tests/test_middleware.py::TestSeveralMiddlewares::test_order_mw_executed_when_exception_in_rsrc tests/test_middleware.py::TestSeveralMiddlewares::test_order_mw_executed_when_exception_in_req tests/test_middleware.py::TestSeveralMiddlewares::test_order_mw_executed_when_exception_in_resp tests/test_middleware.py::TestSeveralMiddlewares::test_outer_mw_with_ex_handler_throw_exception tests/test_middleware.py::TestTransactionIdMiddleware::test_generate_trans_id_with_request tests/test_middleware.py::TestResourceMiddleware::test_can_access_resource_params tests/test_middleware.py::TestRequestTimeMiddleware::test_response_middleware_raises_exception tests/test_middleware.py::TestRequestTimeMiddleware::test_add_invalid_middleware
docs/api/middleware.rst falcon/api.py falcon/api_helpers.py
docs/api/middleware.rst falcon/api.py falcon/api_helpers.py