feat(middleware): Add middleware method "process_resource"
Create a new middleware method process_resource that occurs after process_request and routing, and ensure that the resource object is accessible. Additionally, make the resource object available to process_response. Issue #400
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -35,3 +35,4 @@ below in order of date of first contribution:
|
||||
* Sriram Madapusi Vasudevan (TheSriram)
|
||||
* Erik Erwitt (eerwitt)
|
||||
* Bernhard Weitzhofer (b6d)
|
||||
* Rahman Syed (rsyed83)
|
||||
|
||||
109
doc/api/middleware.rst
Normal file
109
doc/api/middleware.rst
Normal file
@@ -0,0 +1,109 @@
|
||||
.. _middleware:
|
||||
|
||||
Middleware
|
||||
==========
|
||||
|
||||
Middleware components execute both before and after the framework
|
||||
routes the request. Middleware is registered by passing components
|
||||
to the :ref:`API class <api>` initializer.
|
||||
|
||||
The middleware interface is defined as follows:
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
class ExampleComponent(object):
|
||||
def process_request(self, req, resp):
|
||||
"""Process the request before routing it.
|
||||
|
||||
Args:
|
||||
req: Request object that will eventually be
|
||||
routed to an on_* responder method
|
||||
resp: Response object that will be routed to
|
||||
the on_* responder
|
||||
"""
|
||||
|
||||
def process_resource(self, req, resp, resource):
|
||||
"""Process the request after routing.
|
||||
|
||||
Args:
|
||||
req: Request object that will be passed to the
|
||||
routed responder
|
||||
resp: Response object that will be passed to the
|
||||
responder
|
||||
resource: Resource object to which the request was
|
||||
routed. May be None if no route was found for
|
||||
the request
|
||||
"""
|
||||
|
||||
def process_response(self, req, resp, resource)
|
||||
"""Post-processing of the response (after routing).
|
||||
|
||||
Args:
|
||||
req: Request object
|
||||
resp: Response object
|
||||
resource: Resource object to which the request was
|
||||
routed. May be None if no route was found
|
||||
for the request
|
||||
"""
|
||||
|
||||
Because middleware can execute before routing has occurred, if a
|
||||
component modifies ``req.uri`` in its *process_request* method,
|
||||
the framework will use the modified value to route the request.
|
||||
|
||||
Each component's *process_request*, *process_resource*, and
|
||||
*process_response* methods are executed hierarchically, as a stack.
|
||||
For example, if a list of middleware objects are passed as
|
||||
``[mob1, mob2, mob3]``, the order of execution is as follows::
|
||||
|
||||
mob1.process_request
|
||||
mob2.process_request
|
||||
mob3.process_request
|
||||
mob1.process_resource
|
||||
mob2.process_resource
|
||||
mob3.process_resource
|
||||
<route to responder method>
|
||||
mob3.process_response
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
Note that each component need not implement all process_*
|
||||
methods; in the case that one of the three methods is missing,
|
||||
it is treated as a noop in the stack. For example, if ``mob2`` did
|
||||
not implement *process_request* and ``mob3`` did not implement
|
||||
*process_response*, the execution order would look
|
||||
like this::
|
||||
|
||||
mob1.process_request
|
||||
_
|
||||
mob3.process_request
|
||||
mob1.process_resource
|
||||
mob2.process_resource
|
||||
mob3.process_resource
|
||||
<route to responder method>
|
||||
_
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
If one of the *process_request* middleware methods raises an
|
||||
error, it will be processed according to the error type. If
|
||||
the type matches a registered error handler, that handler will
|
||||
be invoked and then the framework will begin to unwind the
|
||||
stack, skipping any lower layers. The error handler may itself
|
||||
raise an instance of HTTPError, in which case the framework
|
||||
will use the latter exception to update the *resp* object.
|
||||
Regardless, the framework will continue unwinding the middleware
|
||||
stack. For example, if *mob2.process_request* were to raise an
|
||||
error, the framework would execute the stack as follows::
|
||||
|
||||
mob1.process_request
|
||||
mob2.process_request
|
||||
<skip mob1/mob2 process_resource, mob3, and routing>
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
Finally, if one of the *process_response* methods raises an error,
|
||||
or the routed on_* responder method itself raises an error, the
|
||||
exception will be handled in a similar manner as above. Then,
|
||||
the framework will execute any remaining middleware on the
|
||||
stack.
|
||||
@@ -115,7 +115,7 @@ Classes and Functions
|
||||
api/request_and_response
|
||||
api/status
|
||||
api/errors
|
||||
api/middleware
|
||||
api/hooks
|
||||
api/routing
|
||||
api/util
|
||||
|
||||
|
||||
109
falcon/api.py
109
falcon/api.py
@@ -37,8 +37,8 @@ class API(object):
|
||||
Args:
|
||||
media_type (str, optional): Default media type to use as the value for
|
||||
the Content-Type header on responses. (default 'application/json')
|
||||
middleware(object or list, optional): One or more objects (
|
||||
instantiated classes) that implement the following middleware
|
||||
middleware(object or list, optional): One or more objects
|
||||
(instantiated classes) that implement the following middleware
|
||||
component interface::
|
||||
|
||||
class ExampleComponent(object):
|
||||
@@ -52,66 +52,30 @@ class API(object):
|
||||
the on_* responder
|
||||
\"""
|
||||
|
||||
def process_response(self, req, resp)
|
||||
\"""Post-processing of the response (after routing).
|
||||
def process_resource(self, req, resp, resource):
|
||||
\"""Process the request after routing.
|
||||
|
||||
Args:
|
||||
req: Request object that will be passed to the
|
||||
routed responder
|
||||
resp: Response object that will be passed to the
|
||||
responder
|
||||
resource: Resource object to which the request was
|
||||
routed. May be None if no route was found for
|
||||
the request
|
||||
\"""
|
||||
|
||||
Middleware components execute both before and after the framework
|
||||
routes the request, or calls any hooks. For example, if a
|
||||
component modifies ``req.uri`` in its *process_request* method,
|
||||
the framework will use the modified value to route the request.
|
||||
|
||||
Each component's *process_request* and *process_response* methods
|
||||
are executed hierarchically, as a stack. For example, if a list of
|
||||
middleware objects are passed as ``[mob1, mob2, mob3]``, the order
|
||||
of execution is as follows::
|
||||
|
||||
mob1.process_request
|
||||
mob2.process_request
|
||||
mob3.process_request
|
||||
<route to responder method>
|
||||
mob3.process_response
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
Note that each component need not implement both process_*
|
||||
methods; in the case that one of the two methods is missing,
|
||||
it is treated as a noop in the stack. For example, if ``mob2`` did
|
||||
not implement *process_request* and ``mob3`` did not implement
|
||||
*process_response*, the execution order would look
|
||||
like this::
|
||||
|
||||
mob1.process_request
|
||||
_
|
||||
mob3.process_request
|
||||
<route to responder method>
|
||||
_
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
If one of the *process_request* middleware methods raises an
|
||||
error, it will be processed according to the error type. If
|
||||
the type matches a registered error handler, that handler will
|
||||
be invoked and then the framework will begin to unwind the
|
||||
stack, skipping any lower layers. The error handler may itself
|
||||
raise an instance of HTTPError, in which case the framework
|
||||
will use the latter exception to update the *resp* object.
|
||||
Regardless, the framework will continue unwinding the middleware
|
||||
stack. For example, if *mob2.process_request* were to raise an
|
||||
error, the framework would execute the stack as follows::
|
||||
|
||||
mob1.process_request
|
||||
mob2.process_request
|
||||
<skip mob3 and routing>
|
||||
mob2.process_response
|
||||
mob1.process_response
|
||||
|
||||
Finally, if one of the *process_response* methods raises an error,
|
||||
or the routed on_* responder method itself raises an error, the
|
||||
exception will be handled in a similar manner as above. Then,
|
||||
the framework will execute any remaining middleware on the
|
||||
stack.
|
||||
def process_response(self, req, resp, resource)
|
||||
\"""Post-processing of the response (after routing).
|
||||
|
||||
Args:
|
||||
req: Request object
|
||||
resp: Response object
|
||||
resource: Resource object to which the request was
|
||||
routed. May be None if no route was found
|
||||
for the request
|
||||
\"""
|
||||
See also :ref:`Middleware <middleware>`.
|
||||
request_type (Request, optional): Request-like class to use instead
|
||||
of Falcon's default class. Among other things, this feature
|
||||
affords inheriting from ``falcon.request.Request`` in order
|
||||
@@ -203,15 +167,18 @@ class API(object):
|
||||
# e.g. a 404.
|
||||
responder, params, resource = self._get_responder(req)
|
||||
|
||||
self._call_rsrc_mw(middleware_stack, req, resp, resource)
|
||||
|
||||
responder(req, resp, **params)
|
||||
self._call_resp_mw(middleware_stack, req, resp)
|
||||
self._call_resp_mw(middleware_stack, req, resp, resource)
|
||||
|
||||
except Exception as ex:
|
||||
for err_type, err_handler in self._error_handlers:
|
||||
if isinstance(ex, err_type):
|
||||
err_handler(ex, req, resp, params)
|
||||
self._call_after_hooks(req, resp, resource)
|
||||
self._call_resp_mw(middleware_stack, req, resp)
|
||||
self._call_resp_mw(middleware_stack, req, resp,
|
||||
resource)
|
||||
|
||||
# NOTE(kgriffs): The following line is not
|
||||
# reported to be covered under Python 3.4 for
|
||||
@@ -232,13 +199,13 @@ class API(object):
|
||||
# process_response when no error_handler is given
|
||||
# and for whatever exception. If an HTTPError is raised
|
||||
# remaining process_response will be executed later.
|
||||
self._call_resp_mw(middleware_stack, req, resp)
|
||||
self._call_resp_mw(middleware_stack, req, resp, resource)
|
||||
raise
|
||||
|
||||
except HTTPError as ex:
|
||||
self._compose_error_response(req, resp, ex)
|
||||
self._call_after_hooks(req, resp, resource)
|
||||
self._call_resp_mw(middleware_stack, req, resp)
|
||||
self._call_resp_mw(middleware_stack, req, resp, resource)
|
||||
|
||||
#
|
||||
# Set status and headers
|
||||
@@ -528,20 +495,28 @@ class API(object):
|
||||
"""Run process_request middleware methods."""
|
||||
|
||||
for component in self._middleware:
|
||||
process_request, _ = component
|
||||
process_request, _, _ = component
|
||||
if process_request is not None:
|
||||
process_request(req, resp)
|
||||
|
||||
# Put executed component on the stack
|
||||
stack.append(component) # keep track from outside
|
||||
|
||||
def _call_resp_mw(self, stack, req, resp):
|
||||
def _call_rsrc_mw(self, stack, req, resp, resource):
|
||||
"""Run process_resource middleware methods."""
|
||||
|
||||
for component in self._middleware:
|
||||
_, process_resource, _ = component
|
||||
if process_resource is not None:
|
||||
process_resource(req, resp, resource)
|
||||
|
||||
def _call_resp_mw(self, stack, req, resp, resource):
|
||||
"""Run process_response middleware."""
|
||||
|
||||
while stack:
|
||||
_, process_response = stack.pop()
|
||||
_, _, process_response = stack.pop()
|
||||
if process_response is not None:
|
||||
process_response(req, resp)
|
||||
process_response(req, resp, resource)
|
||||
|
||||
def _call_after_hooks(self, req, resp, resource):
|
||||
"""Executes each of the global "after" hooks, in turn."""
|
||||
|
||||
@@ -50,14 +50,17 @@ def prepare_middleware(middleware=None):
|
||||
for component in middleware:
|
||||
process_request = util.get_bound_method(component,
|
||||
'process_request')
|
||||
process_resource = util.get_bound_method(component,
|
||||
'process_resource')
|
||||
process_response = util.get_bound_method(component,
|
||||
'process_response')
|
||||
|
||||
if not (process_request or process_response):
|
||||
if not (process_request or process_resource or process_response):
|
||||
msg = '{0} does not implement the middleware interface'
|
||||
raise TypeError(msg.format(component))
|
||||
|
||||
prepared_middleware.append((process_request, process_response))
|
||||
prepared_middleware.append((process_request, process_resource,
|
||||
process_response))
|
||||
|
||||
return prepared_middleware
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class RequestIDComponent(object):
|
||||
def process_request(self, req, resp):
|
||||
req.context['request_id'] = '<generate ID>'
|
||||
|
||||
def process_response(self, req, resp):
|
||||
def process_response(self, req, resp, resource):
|
||||
resp.set_header('X-Request-ID', req.context['request_id'])
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ class RequestTimeMiddleware(object):
|
||||
global context
|
||||
context['start_time'] = datetime.utcnow()
|
||||
|
||||
def process_response(self, req, resp):
|
||||
def process_resource(self, req, resp, resource):
|
||||
global context
|
||||
context['mid_time'] = datetime.utcnow()
|
||||
|
||||
def process_response(self, req, resp, resource):
|
||||
global context
|
||||
context['end_time'] = datetime.utcnow()
|
||||
|
||||
@@ -30,7 +34,12 @@ class ExecutedFirstMiddleware(object):
|
||||
context['executed_methods'].append(
|
||||
'{0}.{1}'.format(self.__class__.__name__, 'process_request'))
|
||||
|
||||
def process_response(self, req, resp):
|
||||
def process_resource(self, req, resp, resource):
|
||||
global context
|
||||
context['executed_methods'].append(
|
||||
'{0}.{1}'.format(self.__class__.__name__, 'process_resource'))
|
||||
|
||||
def process_response(self, req, resp, resource):
|
||||
global context
|
||||
context['executed_methods'].append(
|
||||
'{0}.{1}'.format(self.__class__.__name__, 'process_response'))
|
||||
@@ -81,7 +90,7 @@ class TestRequestTimeMiddleware(TestMiddleware):
|
||||
"""Test that error in response middleware is propagated up"""
|
||||
class RaiseErrorMiddleware(object):
|
||||
|
||||
def process_response(self, req, resp):
|
||||
def process_response(self, req, resp, resource):
|
||||
raise Exception("Always fail")
|
||||
|
||||
self.api = falcon.API(middleware=[RaiseErrorMiddleware()])
|
||||
@@ -101,7 +110,10 @@ class TestRequestTimeMiddleware(TestMiddleware):
|
||||
self.assertEqual([{'status': 'ok'}], body)
|
||||
self.assertEqual(self.srmock.status, falcon.HTTP_200)
|
||||
self.assertIn("start_time", context)
|
||||
self.assertIn("mid_time", context)
|
||||
self.assertIn("end_time", context)
|
||||
self.assertTrue(context['mid_time'] > context['start_time'],
|
||||
"process_resource not executed after request")
|
||||
self.assertTrue(context['end_time'] > context['start_time'],
|
||||
"process_response not executed after request")
|
||||
|
||||
@@ -137,7 +149,10 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
self.assertIn("transaction_id", context)
|
||||
self.assertEqual("unique-req-id", context['transaction_id'])
|
||||
self.assertIn("start_time", context)
|
||||
self.assertIn("mid_time", context)
|
||||
self.assertIn("end_time", context)
|
||||
self.assertTrue(context['mid_time'] > context['start_time'],
|
||||
"process_resource not executed after request")
|
||||
self.assertTrue(context['end_time'] > context['start_time'],
|
||||
"process_response not executed after request")
|
||||
|
||||
@@ -156,6 +171,8 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
expectedExecutedMethods = [
|
||||
"ExecutedFirstMiddleware.process_request",
|
||||
"ExecutedLastMiddleware.process_request",
|
||||
"ExecutedFirstMiddleware.process_resource",
|
||||
"ExecutedLastMiddleware.process_resource",
|
||||
"ExecutedLastMiddleware.process_response",
|
||||
"ExecutedFirstMiddleware.process_response"
|
||||
]
|
||||
@@ -181,6 +198,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
# RequestTimeMiddleware process_response should be executed
|
||||
self.assertIn("transaction_id", context)
|
||||
self.assertIn("start_time", context)
|
||||
self.assertNotIn("mid_time", context)
|
||||
self.assertIn("end_time", context)
|
||||
|
||||
def test_inner_mw_with_ex_handler_throw_exception(self):
|
||||
@@ -189,7 +207,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
|
||||
class RaiseErrorMiddleware(object):
|
||||
|
||||
def process_request(self, req, resp):
|
||||
def process_request(self, req, resp, resource):
|
||||
raise Exception("Always fail")
|
||||
|
||||
self.api = falcon.API(middleware=[TransactionIdMiddleware(),
|
||||
@@ -208,6 +226,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
# RequestTimeMiddleware process_response should be executed
|
||||
self.assertIn("transaction_id", context)
|
||||
self.assertIn("start_time", context)
|
||||
self.assertNotIn("mid_time", context)
|
||||
self.assertIn("end_time", context)
|
||||
self.assertIn("error_handler", context)
|
||||
|
||||
@@ -236,6 +255,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
# Any mw is executed now...
|
||||
self.assertIn("transaction_id", context)
|
||||
self.assertNotIn("start_time", context)
|
||||
self.assertNotIn("mid_time", context)
|
||||
self.assertNotIn("end_time", context)
|
||||
self.assertIn("error_handler", context)
|
||||
|
||||
@@ -245,7 +265,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
|
||||
class RaiseErrorMiddleware(object):
|
||||
|
||||
def process_response(self, req, resp):
|
||||
def process_response(self, req, resp, resource):
|
||||
raise Exception("Always fail")
|
||||
|
||||
self.api = falcon.API(middleware=[ExecutedFirstMiddleware(),
|
||||
@@ -253,7 +273,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
ExecutedLastMiddleware()])
|
||||
|
||||
def handler(ex, req, resp, params):
|
||||
context['error_handler'] = True
|
||||
pass
|
||||
|
||||
self.api.add_error_handler(Exception, handler)
|
||||
|
||||
@@ -265,6 +285,8 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
expectedExecutedMethods = [
|
||||
"ExecutedFirstMiddleware.process_request",
|
||||
"ExecutedLastMiddleware.process_request",
|
||||
"ExecutedFirstMiddleware.process_resource",
|
||||
"ExecutedLastMiddleware.process_resource",
|
||||
"ExecutedLastMiddleware.process_response",
|
||||
"ExecutedFirstMiddleware.process_response"
|
||||
]
|
||||
@@ -284,7 +306,7 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
ExecutedLastMiddleware()])
|
||||
|
||||
def handler(ex, req, resp, params):
|
||||
context['error_handler'] = True
|
||||
pass
|
||||
|
||||
self.api.add_error_handler(Exception, handler)
|
||||
|
||||
@@ -299,6 +321,38 @@ class TestSeveralMiddlewares(TestMiddleware):
|
||||
]
|
||||
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
|
||||
|
||||
class RaiseErrorMiddleware(object):
|
||||
|
||||
def process_resource(self, req, resp, resource):
|
||||
raise Exception("Always fail")
|
||||
|
||||
self.api = falcon.API(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