diff --git a/AUTHORS b/AUTHORS index 416272a31..6d7272baa 100644 --- a/AUTHORS +++ b/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) diff --git a/doc/api/middleware.rst b/doc/api/middleware.rst new file mode 100644 index 000000000..f3ca39301 --- /dev/null +++ b/doc/api/middleware.rst @@ -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 ` 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 + + 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 + + _ + 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 + + 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. diff --git a/doc/index.rst b/doc/index.rst index c8af719b0..574545456 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -115,7 +115,7 @@ Classes and Functions api/request_and_response api/status api/errors + api/middleware api/hooks api/routing api/util - diff --git a/falcon/api.py b/falcon/api.py index f00a63afb..d04c799a6 100644 --- a/falcon/api.py +++ b/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 @@ def process_request(self, req, resp): 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 - - 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 - - _ - 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 - - 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 `. 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 @@ def __call__(self, env, start_response): # 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 @@ def __call__(self, env, start_response): # 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 @@ def _call_req_mw(self, stack, req, resp): """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.""" diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index 311a6b0f1..35b0e5d02 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -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 diff --git a/falcon/bench/queues/api.py b/falcon/bench/queues/api.py index feb3864b5..fa6143556 100644 --- a/falcon/bench/queues/api.py +++ b/falcon/bench/queues/api.py @@ -25,7 +25,7 @@ class RequestIDComponent(object): def process_request(self, req, resp): req.context['request_id'] = '' - def process_response(self, req, resp): + def process_response(self, req, resp, resource): resp.set_header('X-Request-ID', req.context['request_id']) diff --git a/tests/test_middlewares.py b/tests/test_middlewares.py index b9cac1f04..9c37e3496 100644 --- a/tests/test_middlewares.py +++ b/tests/test_middlewares.py @@ -11,7 +11,11 @@ def process_request(self, req, resp): 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 @@ def process_request(self, req, resp): 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 @@ def test_response_middleware_raises_exception(self): """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 @@ def test_log_get_request(self): 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 @@ def test_generate_trans_id_and_time_with_request(self): 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 @@ def test_middleware_execution_order(self): expectedExecutedMethods = [ "ExecutedFirstMiddleware.process_request", "ExecutedLastMiddleware.process_request", + "ExecutedFirstMiddleware.process_resource", + "ExecutedLastMiddleware.process_resource", "ExecutedLastMiddleware.process_response", "ExecutedFirstMiddleware.process_response" ] @@ -181,6 +198,7 @@ def process_request(self, req, resp): # 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 @@ def test_inner_mw_with_ex_handler_throw_exception(self): 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 @@ def handler(ex, req, resp, params): # 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 @@ def handler(ex, req, resp, params): # 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 @@ def test_order_mw_executed_when_exception_in_resp(self): 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 @@ def process_response(self, req, resp): ExecutedLastMiddleware()]) def handler(ex, req, resp, params): - context['error_handler'] = True + pass self.api.add_error_handler(Exception, handler) @@ -265,6 +285,8 @@ def handler(ex, req, resp, params): expectedExecutedMethods = [ "ExecutedFirstMiddleware.process_request", "ExecutedLastMiddleware.process_request", + "ExecutedFirstMiddleware.process_resource", + "ExecutedLastMiddleware.process_resource", "ExecutedLastMiddleware.process_response", "ExecutedFirstMiddleware.process_response" ] @@ -284,7 +306,36 @@ def process_request(self, req, resp): ExecutedLastMiddleware()]) def handler(ex, req, resp, params): - context['error_handler'] = True + 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", + "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 + + 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) @@ -295,6 +346,9 @@ def handler(ex, req, resp, params): # 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'])