feat(middleware): Optional, independent execution of request and response middleware (#926)

This commit is contained in:
Matt Giles 2017-01-24 10:43:21 -08:00 committed by Kurt Griffiths
parent 883898ad27
commit 33d35c6893
3 changed files with 172 additions and 23 deletions

View File

@ -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:

View File

@ -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):

View File

@ -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):