feat(middleware): Optional, independent execution of request and response middleware (#926)
This commit is contained in:
		 Matt Giles
					Matt Giles
				
			
				
					committed by
					
						 Kurt Griffiths
						Kurt Griffiths
					
				
			
			
				
	
			
			
			 Kurt Griffiths
						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