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.
|
to use in lieu of the default engine.
|
||||||
See also: :ref:`Routing <routing>`.
|
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:
|
Attributes:
|
||||||
req_options: A set of behavioral options related to incoming
|
req_options: A set of behavioral options related to incoming
|
||||||
requests. See also: :py:class:`~.RequestOptions`
|
requests. See also: :py:class:`~.RequestOptions`
|
||||||
@@ -121,16 +125,20 @@ class API(object):
|
|||||||
|
|
||||||
__slots__ = ('_request_type', '_response_type',
|
__slots__ = ('_request_type', '_response_type',
|
||||||
'_error_handlers', '_media_type', '_router', '_sinks',
|
'_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,
|
def __init__(self, media_type=DEFAULT_MEDIA_TYPE,
|
||||||
request_type=Request, response_type=Response,
|
request_type=Request, response_type=Response,
|
||||||
middleware=None, router=None):
|
middleware=None, router=None,
|
||||||
|
independent_middleware=False):
|
||||||
self._sinks = []
|
self._sinks = []
|
||||||
self._media_type = media_type
|
self._media_type = media_type
|
||||||
|
|
||||||
# set middleware
|
# 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()
|
self._router = router or routing.DefaultRouter()
|
||||||
|
|
||||||
@@ -166,7 +174,9 @@ class API(object):
|
|||||||
resource = None
|
resource = None
|
||||||
params = {}
|
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
|
req_succeeded = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -174,13 +184,18 @@ class API(object):
|
|||||||
# NOTE(ealogar): The execution of request middleware
|
# NOTE(ealogar): The execution of request middleware
|
||||||
# should be before routing. This will allow request mw
|
# should be before routing. This will allow request mw
|
||||||
# to modify the path.
|
# to modify the path.
|
||||||
for component in self._middleware:
|
# NOTE: if flag set to use independent middleware, execute
|
||||||
process_request, _, process_response = component
|
# request middleware independently. Otherwise, only queue
|
||||||
if process_request is not None:
|
# response middleware after request middleware succeeds.
|
||||||
|
if self._independent_middleware:
|
||||||
|
for process_request in mw_req_stack:
|
||||||
process_request(req, resp)
|
process_request(req, resp)
|
||||||
|
else:
|
||||||
if process_response is not None:
|
for process_request, process_response in mw_req_stack:
|
||||||
mw_pr_stack.append(process_response)
|
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
|
# NOTE(warsaw): Moved this to inside the try except
|
||||||
# because it is possible when using object-based
|
# because it is possible when using object-based
|
||||||
@@ -201,10 +216,8 @@ class API(object):
|
|||||||
# resource middleware methods.
|
# resource middleware methods.
|
||||||
if resource is not None:
|
if resource is not None:
|
||||||
# Call process_resource middleware methods.
|
# Call process_resource middleware methods.
|
||||||
for component in self._middleware:
|
for process_resource in mw_rsrc_stack:
|
||||||
_, process_resource, _ = component
|
process_resource(req, resp, resource, params)
|
||||||
if process_resource is not None:
|
|
||||||
process_resource(req, resp, resource, params)
|
|
||||||
|
|
||||||
responder(req, resp, **params)
|
responder(req, resp, **params)
|
||||||
req_succeeded = True
|
req_succeeded = True
|
||||||
@@ -220,8 +233,7 @@ class API(object):
|
|||||||
# reworked.
|
# reworked.
|
||||||
|
|
||||||
# Call process_response middleware methods.
|
# Call process_response middleware methods.
|
||||||
while mw_pr_stack:
|
for process_response in mw_resp_stack or dependent_mw_resp_stack:
|
||||||
process_response = mw_pr_stack.pop()
|
|
||||||
try:
|
try:
|
||||||
process_response(req, resp, resource, req_succeeded)
|
process_response(req, resp, resource, req_succeeded)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
@@ -19,19 +19,23 @@ from functools import wraps
|
|||||||
from falcon import util
|
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.
|
"""Check middleware interface and prepare it to iterate.
|
||||||
|
|
||||||
Args:
|
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:
|
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
|
# PERF(kgriffs): do getattr calls once, in advance, so we don't
|
||||||
# have to do them every time in the request path.
|
# have to do them every time in the request path.
|
||||||
prepared_middleware = []
|
request_mw = []
|
||||||
|
resource_mw = []
|
||||||
|
response_mw = []
|
||||||
|
|
||||||
if middleware is None:
|
if middleware is None:
|
||||||
middleware = []
|
middleware = []
|
||||||
@@ -66,10 +70,22 @@ def prepare_middleware(middleware=None):
|
|||||||
|
|
||||||
process_response = let()
|
process_response = let()
|
||||||
|
|
||||||
prepared_middleware.append((process_request, process_resource,
|
# NOTE: depending on whether we want to execute middleware
|
||||||
process_response))
|
# 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):
|
def default_serialize_error(req, resp, exception):
|
||||||
|
|||||||
@@ -247,6 +247,29 @@ class TestSeveralMiddlewares(TestMiddleware):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expectedExecutedMethods, context['executed_methods'])
|
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):
|
def test_multiple_reponse_mw_throw_exception(self):
|
||||||
"""Test that error in inner middleware leaves"""
|
"""Test that error in inner middleware leaves"""
|
||||||
global context
|
global context
|
||||||
@@ -395,6 +418,40 @@ class TestSeveralMiddlewares(TestMiddleware):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expectedExecutedMethods, context['executed_methods'])
|
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):
|
def test_order_mw_executed_when_exception_in_req(self):
|
||||||
"""Test that error in inner middleware leaves"""
|
"""Test that error in inner middleware leaves"""
|
||||||
global context
|
global context
|
||||||
@@ -424,6 +481,37 @@ class TestSeveralMiddlewares(TestMiddleware):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expectedExecutedMethods, context['executed_methods'])
|
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):
|
def test_order_mw_executed_when_exception_in_rsrc(self):
|
||||||
"""Test that error in inner middleware leaves"""
|
"""Test that error in inner middleware leaves"""
|
||||||
global context
|
global context
|
||||||
@@ -456,6 +544,39 @@ class TestSeveralMiddlewares(TestMiddleware):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expectedExecutedMethods, context['executed_methods'])
|
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):
|
class TestRemoveBasePathMiddleware(TestMiddleware):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user