feat(middleware): Optional, independent execution of request and response middleware (#926)
This commit is contained in:
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):
|
||||
|
||||
|
|
Loading…
Reference in New Issue