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:
Rahman Syed
2015-01-26 16:04:05 -06:00
parent fba36327f1
commit ead6357fd5
7 changed files with 220 additions and 78 deletions

View File

@@ -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
View 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.

View File

@@ -115,7 +115,7 @@ Classes and Functions
api/request_and_response
api/status
api/errors
api/middleware
api/hooks
api/routing
api/util

View File

@@ -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."""

View File

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

View File

@@ -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'])

View File

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