feat(middleware): Optional, independent execution of request and response middleware (#926)
This commit is contained in:
		
				
					committed by
					
						
						Kurt Griffiths
					
				
			
			
				
	
			
			
			
						parent
						
							883898ad27
						
					
				
				
					commit
					33d35c6893
				
			@@ -103,6 +103,10 @@ class API(object):
 | 
			
		||||
            to use in lieu of the default engine.
 | 
			
		||||
            See also: :ref:`Routing <routing>`.
 | 
			
		||||
 | 
			
		||||
        independent_middleware (bool): set to true if response middleware
 | 
			
		||||
            should be executed independently of whether or not request
 | 
			
		||||
            middleware raises an exception.
 | 
			
		||||
 | 
			
		||||
    Attributes:
 | 
			
		||||
        req_options: A set of behavioral options related to incoming
 | 
			
		||||
            requests. See also: :py:class:`~.RequestOptions`
 | 
			
		||||
@@ -121,16 +125,20 @@ class API(object):
 | 
			
		||||
 | 
			
		||||
    __slots__ = ('_request_type', '_response_type',
 | 
			
		||||
                 '_error_handlers', '_media_type', '_router', '_sinks',
 | 
			
		||||
                 '_serialize_error', 'req_options', '_middleware')
 | 
			
		||||
                 '_serialize_error', 'req_options',
 | 
			
		||||
                 '_middleware', '_independent_middleware')
 | 
			
		||||
 | 
			
		||||
    def __init__(self, media_type=DEFAULT_MEDIA_TYPE,
 | 
			
		||||
                 request_type=Request, response_type=Response,
 | 
			
		||||
                 middleware=None, router=None):
 | 
			
		||||
                 middleware=None, router=None,
 | 
			
		||||
                 independent_middleware=False):
 | 
			
		||||
        self._sinks = []
 | 
			
		||||
        self._media_type = media_type
 | 
			
		||||
 | 
			
		||||
        # set middleware
 | 
			
		||||
        self._middleware = helpers.prepare_middleware(middleware)
 | 
			
		||||
        self._middleware = helpers.prepare_middleware(
 | 
			
		||||
            middleware, independent_middleware=independent_middleware)
 | 
			
		||||
        self._independent_middleware = independent_middleware
 | 
			
		||||
 | 
			
		||||
        self._router = router or routing.DefaultRouter()
 | 
			
		||||
 | 
			
		||||
@@ -166,7 +174,9 @@ class API(object):
 | 
			
		||||
        resource = None
 | 
			
		||||
        params = {}
 | 
			
		||||
 | 
			
		||||
        mw_pr_stack = []  # Keep track of executed middleware components
 | 
			
		||||
        dependent_mw_resp_stack = []
 | 
			
		||||
        mw_req_stack, mw_rsrc_stack, mw_resp_stack = self._middleware
 | 
			
		||||
 | 
			
		||||
        req_succeeded = False
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
@@ -174,13 +184,18 @@ class API(object):
 | 
			
		||||
                # NOTE(ealogar): The execution of request middleware
 | 
			
		||||
                # should be before routing. This will allow request mw
 | 
			
		||||
                # to modify the path.
 | 
			
		||||
                for component in self._middleware:
 | 
			
		||||
                    process_request, _, process_response = component
 | 
			
		||||
                    if process_request is not None:
 | 
			
		||||
                # NOTE: if flag set to use independent middleware, execute
 | 
			
		||||
                # request middleware independently. Otherwise, only queue
 | 
			
		||||
                # response middleware after request middleware succeeds.
 | 
			
		||||
                if self._independent_middleware:
 | 
			
		||||
                    for process_request in mw_req_stack:
 | 
			
		||||
                        process_request(req, resp)
 | 
			
		||||
 | 
			
		||||
                    if process_response is not None:
 | 
			
		||||
                        mw_pr_stack.append(process_response)
 | 
			
		||||
                else:
 | 
			
		||||
                    for process_request, process_response in mw_req_stack:
 | 
			
		||||
                        if process_request:
 | 
			
		||||
                            process_request(req, resp)
 | 
			
		||||
                        if process_response:
 | 
			
		||||
                            dependent_mw_resp_stack.insert(0, process_response)
 | 
			
		||||
 | 
			
		||||
                # NOTE(warsaw): Moved this to inside the try except
 | 
			
		||||
                # because it is possible when using object-based
 | 
			
		||||
@@ -201,10 +216,8 @@ class API(object):
 | 
			
		||||
                    # resource middleware methods.
 | 
			
		||||
                    if resource is not None:
 | 
			
		||||
                        # Call process_resource middleware methods.
 | 
			
		||||
                        for component in self._middleware:
 | 
			
		||||
                            _, process_resource, _ = component
 | 
			
		||||
                            if process_resource is not None:
 | 
			
		||||
                                process_resource(req, resp, resource, params)
 | 
			
		||||
                        for process_resource in mw_rsrc_stack:
 | 
			
		||||
                            process_resource(req, resp, resource, params)
 | 
			
		||||
 | 
			
		||||
                    responder(req, resp, **params)
 | 
			
		||||
                    req_succeeded = True
 | 
			
		||||
@@ -220,8 +233,7 @@ class API(object):
 | 
			
		||||
            # reworked.
 | 
			
		||||
 | 
			
		||||
            # Call process_response middleware methods.
 | 
			
		||||
            while mw_pr_stack:
 | 
			
		||||
                process_response = mw_pr_stack.pop()
 | 
			
		||||
            for process_response in mw_resp_stack or dependent_mw_resp_stack:
 | 
			
		||||
                try:
 | 
			
		||||
                    process_response(req, resp, resource, req_succeeded)
 | 
			
		||||
                except Exception as ex:
 | 
			
		||||
 
 | 
			
		||||
@@ -19,19 +19,23 @@ from functools import wraps
 | 
			
		||||
from falcon import util
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def prepare_middleware(middleware=None):
 | 
			
		||||
def prepare_middleware(middleware=None, independent_middleware=False):
 | 
			
		||||
    """Check middleware interface and prepare it to iterate.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        middleware:  list (or object) of input middleware
 | 
			
		||||
        middleware: list (or object) of input middleware
 | 
			
		||||
        independent_middleware: bool whether should prepare request and
 | 
			
		||||
            response middleware independently
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        list: A list of prepared middleware tuples
 | 
			
		||||
        list: A tuple of prepared middleware tuples
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # PERF(kgriffs): do getattr calls once, in advance, so we don't
 | 
			
		||||
    # have to do them every time in the request path.
 | 
			
		||||
    prepared_middleware = []
 | 
			
		||||
    request_mw = []
 | 
			
		||||
    resource_mw = []
 | 
			
		||||
    response_mw = []
 | 
			
		||||
 | 
			
		||||
    if middleware is None:
 | 
			
		||||
        middleware = []
 | 
			
		||||
@@ -66,10 +70,22 @@ def prepare_middleware(middleware=None):
 | 
			
		||||
 | 
			
		||||
                process_response = let()
 | 
			
		||||
 | 
			
		||||
        prepared_middleware.append((process_request, process_resource,
 | 
			
		||||
                                    process_response))
 | 
			
		||||
        # NOTE: depending on whether we want to execute middleware
 | 
			
		||||
        # independently, we group response and request middleware either
 | 
			
		||||
        # together or separately.
 | 
			
		||||
        if independent_middleware:
 | 
			
		||||
            if process_request:
 | 
			
		||||
                request_mw.append(process_request)
 | 
			
		||||
            if process_response:
 | 
			
		||||
                response_mw.insert(0, process_response)
 | 
			
		||||
        else:
 | 
			
		||||
            if process_request or process_response:
 | 
			
		||||
                request_mw.append((process_request, process_response))
 | 
			
		||||
 | 
			
		||||
    return prepared_middleware
 | 
			
		||||
        if process_resource:
 | 
			
		||||
            resource_mw.append(process_resource)
 | 
			
		||||
 | 
			
		||||
    return (tuple(request_mw), tuple(resource_mw), tuple(response_mw))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_serialize_error(req, resp, exception):
 | 
			
		||||
 
 | 
			
		||||
@@ -247,6 +247,29 @@ class TestSeveralMiddlewares(TestMiddleware):
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_independent_middleware_execution_order(self):
 | 
			
		||||
        global context
 | 
			
		||||
        self.api = falcon.API(independent_middleware=True,
 | 
			
		||||
                              middleware=[ExecutedFirstMiddleware(),
 | 
			
		||||
                                          ExecutedLastMiddleware()])
 | 
			
		||||
 | 
			
		||||
        self.api.add_route(self.test_route, MiddlewareClassResource())
 | 
			
		||||
 | 
			
		||||
        body = self.simulate_json_request(self.test_route)
 | 
			
		||||
        self.assertEqual(_EXPECTED_BODY, body)
 | 
			
		||||
        self.assertEqual(self.srmock.status, falcon.HTTP_200)
 | 
			
		||||
        # as the method registration is in a list, the order also is
 | 
			
		||||
        # tested
 | 
			
		||||
        expectedExecutedMethods = [
 | 
			
		||||
            'ExecutedFirstMiddleware.process_request',
 | 
			
		||||
            'ExecutedLastMiddleware.process_request',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_resource',
 | 
			
		||||
            'ExecutedLastMiddleware.process_resource',
 | 
			
		||||
            'ExecutedLastMiddleware.process_response',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_response'
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_multiple_reponse_mw_throw_exception(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
@@ -395,6 +418,40 @@ class TestSeveralMiddlewares(TestMiddleware):
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_order_independent_mw_executed_when_exception_in_resp(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
 | 
			
		||||
        class RaiseErrorMiddleware(object):
 | 
			
		||||
 | 
			
		||||
            def process_response(self, req, resp, resource):
 | 
			
		||||
                raise Exception('Always fail')
 | 
			
		||||
 | 
			
		||||
        self.api = falcon.API(independent_middleware=True,
 | 
			
		||||
                              middleware=[ExecutedFirstMiddleware(),
 | 
			
		||||
                                          RaiseErrorMiddleware(),
 | 
			
		||||
                                          ExecutedLastMiddleware()])
 | 
			
		||||
 | 
			
		||||
        def handler(ex, req, resp, params):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        self.api.add_error_handler(Exception, handler)
 | 
			
		||||
 | 
			
		||||
        self.api.add_route(self.test_route, MiddlewareClassResource())
 | 
			
		||||
 | 
			
		||||
        self.simulate_request(self.test_route)
 | 
			
		||||
 | 
			
		||||
        # Any mw is executed now...
 | 
			
		||||
        expectedExecutedMethods = [
 | 
			
		||||
            'ExecutedFirstMiddleware.process_request',
 | 
			
		||||
            'ExecutedLastMiddleware.process_request',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_resource',
 | 
			
		||||
            'ExecutedLastMiddleware.process_resource',
 | 
			
		||||
            'ExecutedLastMiddleware.process_response',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_response'
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_order_mw_executed_when_exception_in_req(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
@@ -424,6 +481,37 @@ class TestSeveralMiddlewares(TestMiddleware):
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_order_independent_mw_executed_when_exception_in_req(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
 | 
			
		||||
        class RaiseErrorMiddleware(object):
 | 
			
		||||
 | 
			
		||||
            def process_request(self, req, resp):
 | 
			
		||||
                raise Exception('Always fail')
 | 
			
		||||
 | 
			
		||||
        self.api = falcon.API(independent_middleware=True,
 | 
			
		||||
                              middleware=[ExecutedFirstMiddleware(),
 | 
			
		||||
                                          RaiseErrorMiddleware(),
 | 
			
		||||
                                          ExecutedLastMiddleware()])
 | 
			
		||||
 | 
			
		||||
        def handler(ex, req, resp, params):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        self.api.add_error_handler(Exception, handler)
 | 
			
		||||
 | 
			
		||||
        self.api.add_route(self.test_route, MiddlewareClassResource())
 | 
			
		||||
 | 
			
		||||
        self.simulate_request(self.test_route)
 | 
			
		||||
 | 
			
		||||
        # All response middleware still executed...
 | 
			
		||||
        expectedExecutedMethods = [
 | 
			
		||||
            'ExecutedFirstMiddleware.process_request',
 | 
			
		||||
            'ExecutedLastMiddleware.process_response',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_response'
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_order_mw_executed_when_exception_in_rsrc(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
@@ -456,6 +544,39 @@ class TestSeveralMiddlewares(TestMiddleware):
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
    def test_order_independent_mw_executed_when_exception_in_rsrc(self):
 | 
			
		||||
        """Test that error in inner middleware leaves"""
 | 
			
		||||
        global context
 | 
			
		||||
 | 
			
		||||
        class RaiseErrorMiddleware(object):
 | 
			
		||||
 | 
			
		||||
            def process_resource(self, req, resp, resource):
 | 
			
		||||
                raise Exception('Always fail')
 | 
			
		||||
 | 
			
		||||
        self.api = falcon.API(independent_middleware=True,
 | 
			
		||||
                              middleware=[ExecutedFirstMiddleware(),
 | 
			
		||||
                                          RaiseErrorMiddleware(),
 | 
			
		||||
                                          ExecutedLastMiddleware()])
 | 
			
		||||
 | 
			
		||||
        def handler(ex, req, resp, params):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        self.api.add_error_handler(Exception, handler)
 | 
			
		||||
 | 
			
		||||
        self.api.add_route(self.test_route, MiddlewareClassResource())
 | 
			
		||||
 | 
			
		||||
        self.simulate_request(self.test_route)
 | 
			
		||||
 | 
			
		||||
        # Any mw is executed now...
 | 
			
		||||
        expectedExecutedMethods = [
 | 
			
		||||
            'ExecutedFirstMiddleware.process_request',
 | 
			
		||||
            'ExecutedLastMiddleware.process_request',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_resource',
 | 
			
		||||
            'ExecutedLastMiddleware.process_response',
 | 
			
		||||
            'ExecutedFirstMiddleware.process_response'
 | 
			
		||||
        ]
 | 
			
		||||
        self.assertEqual(expectedExecutedMethods, context['executed_methods'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRemoveBasePathMiddleware(TestMiddleware):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user