Skip to content

Commit

Permalink
ASGI: Update middleware docs (#1662)
Browse files Browse the repository at this point in the history
Partially-Implements: #1358
  • Loading branch information
kgriffs authored Feb 15, 2020
1 parent 61d6424 commit 40f1ac7
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 23 deletions.
124 changes: 121 additions & 3 deletions docs/api/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ when instantiating Falcon's :ref:`App class <app>`.
.. Note::
Unlike hooks, middleware methods apply globally to the entire App.

Falcon's middleware interface is defined as follows:
Falcon's WSGI middleware interface is defined as follows:

.. code:: python
Expand Down Expand Up @@ -66,6 +66,124 @@ Falcon's middleware interface is defined as follows:
otherwise False.
"""
The ASGI middleware interface is similar, but also supports the
standard ASGI lifespan events. However, because lifespan events are an
optional part of the ASGI specification, they may or may not fire depending
on your ASGI server:

.. code:: python
class ExampleComponent:
async def process_startup(self, scope, event):
"""Process the ASGI lifespan startup event.
Invoked when the server is ready to start up and
receive connections, but before it has started to
do so.
To halt startup processing and signal to the server that it
should terminate, simply raise an exception and the
framework will convert it to a "lifespan.startup.failed"
event for the server.
Args:
scope (dict): The ASGI scope dictionary for the
lifespan protocol. The lifespan scope exists
for the duration of the event loop.
event (dict): The ASGI event dictionary for the
startup event.
"""
async def process_shutdown(self, scope, event):
"""Process the ASGI lifespan shutdown event.
Invoked when the server has stopped accepting
connections and closed all active connections.
To halt shutdown processing and signal to the server
that it should immediately terminate, simply raise an
exception and the framework will convert it to a
"lifespan.shutdown.failed" event for the server.
Args:
scope (dict): The ASGI scope dictionary for the
lifespan protocol. The lifespan scope exists
for the duration of the event loop.
event (dict): The ASGI event dictionary for the
shutdown event.
"""
async def process_request(self, req, resp):
"""Process the request before routing it.
Note:
Because Falcon routes each request based on req.path, a
request can be effectively re-routed by setting that
attribute to a new value from within process_request().
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.
"""
async def process_resource(self, req, resp, resource, params):
"""Process the request after routing.
Note:
This method is only called when the request matches
a route to a resource.
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.
params: A dict-like object representing any additional
params derived from the route's URI template fields,
that will be passed to the resource's responder
method as keyword arguments.
"""
async def process_response(self, req, resp, resource, req_succeeded):
"""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.
req_succeeded: True if no exceptions were raised while
the framework processed and routed the request;
otherwise False.
"""
It is also possible to implement a middleware component that is compatible
with both ASGI and WSGI apps. This is done by applying an `*_async` postfix
to distinguish the two different versions of each middleware method, as in
the following example:

.. code:: python
class ExampleComponent:
def process_request(self, req, resp):
"""Process WSGI request using synchronous logic.
Note that req and resp are instances of falcon.Request and
falcon.Response, respectively.
"""
async def process_request_async(self, req, resp):
"""Process ASGI request using asynchronous logic.
Note that req and resp are instances of falcon.asgi.Request and
falcon.asgi.Response, respectively.
"""
.. Tip::
Because *process_request* executes before routing has occurred, if a
component modifies ``req.path`` in its *process_request* method,
Expand All @@ -90,7 +208,7 @@ Falcon's middleware interface is defined as follows:
Each component's *process_request*, *process_resource*, and
*process_response* methods are executed hierarchically, as a stack, following
the ordering of the list passed via the `middleware` kwarg of
:ref:`falcon.App<app>`. For example, if a list of middleware objects are
:class:`falcon.App` or :class:`falcon.asgi.App`. For example, if a list of middleware objects are
passed as ``[mob1, mob2, mob3]``, the order of execution is as follows::

mob1.process_request
Expand Down Expand Up @@ -175,7 +293,7 @@ error, the framework would execute the stack as follows::
mob1.process_response

As illustrated above, by default, all *process_response* methods will be
executed, even when a *process_request*, *process_resource*, or resource
executed, even when a *process_request*, *process_resource*, or *on_\** resource
responder raises an error. This behavior is controlled by the
:ref:`App class's <app>` `independent_middleware` keyword argument.

Expand Down
3 changes: 2 additions & 1 deletion falcon/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class App:
Each App instance provides a callable
`WSGI <https://www.python.org/dev/peps/pep-3333/>`_ interface
and a routing engine.
and a routing engine (for ASGI applications, see
:class:`falcon.asgi.App`).
Note:
The ``API`` class was renamed to ``App`` in Falcon 3.0. The
Expand Down
15 changes: 3 additions & 12 deletions falcon/app_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,9 @@ def prepare_middleware(middleware, independent_middleware=False, asgi=False):
response_mw = []

for component in middleware:
# NOTE(kgriffs): Middleware that uses parts of the Request and Response
# interfaces that are the same between ASGI and WSGI (most of it is,
# and we should probably define this via ABC) can just implement
# the method names without the *_async postfix. If a middleware
# component wants to provide an alternative implementation that
# does some work that requires async def, or something specific about
# the ASGI Request/Response classes, the component can implement the
# *_async method in that case.
#
# Middleware that is WSGI-only or ASGI-only can simply implement all
# methods without the *_async postfix. Regardless, components should
# clearly document their compatibility with WSGI vs. ASGI.
# NOTE(kgriffs): Middleware that supports both WSGI and ASGI can
# append an *_async postfix to the ASGI version of the method
# to distinguish the two. Otherwise, the prefix is unnecessary.

if asgi:
process_request = (
Expand Down
25 changes: 18 additions & 7 deletions falcon/asgi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class App(falcon.app.App):
Each App instance provides a callable
`ASGI <https://asgi.readthedocs.io/en/latest/>`_ interface
and a routing engine.
and a routing engine (for WSGI applications, see
:class:`falcon.App`).
Keyword Arguments:
media_type (str): Default media type to use when initializing
Expand All @@ -63,11 +64,13 @@ class App(falcon.app.App):
such as ``falcon.MEDIA_MSGPACK``, ``falcon.MEDIA_YAML``,
``falcon.MEDIA_XML``, etc.
middleware: Either a single middleware component object or an iterable
of objects (instantiated classes) that implement the following
middleware component interface.
of objects (instantiated classes) that implement the
middleware component interface shown below.
The interface provides support for handling both ASGI worker
lifespan events and per-request events.
lifespan events and per-request events. However, because lifespan
events are an optional part of the ASGI specification, they may or
may not fire depending on your ASGI server.
A lifespan event handler can be used to perform startup or shutdown
activities for the main event loop. An example of this would be
Expand All @@ -79,6 +82,14 @@ class App(falcon.app.App):
triggered independently for the individual event loop associated
with each process.
Note:
The framework requires that all middleware methods be
implemented as coroutine functions via `async def`. However,
it is possible to implement middleware classes that support
both ASGI and WSGI apps by distinguishing the ASGI methods
with an `*_async` postfix (see also:
:ref:`Middleware <middleware>`).
It is only necessary to implement the methods for the events you
would like to handle; Falcon simply skips over any missing
middleware methods::
Expand All @@ -87,7 +98,7 @@ class ExampleComponent:
async def process_startup(self, scope, event):
\"\"\"Process the ASGI lifespan startup event.
Invoked when the server is ready to startup and
Invoked when the server is ready to start up and
receive connections, but before it has started to
do so.
Expand All @@ -96,7 +107,7 @@ async def process_startup(self, scope, event):
framework will convert it to a "lifespan.startup.failed"
event for the server.
Arguments:
Args:
scope (dict): The ASGI scope dictionary for the
lifespan protocol. The lifespan scope exists
for the duration of the event loop.
Expand All @@ -115,7 +126,7 @@ async def process_shutdown(self, scope, event):
exception and the framework will convert it to a
"lifespan.shutdown.failed" event for the server.
Arguments:
Args:
scope (dict): The ASGI scope dictionary for the
lifespan protocol. The lifespan scope exists
for the duration of the event loop.
Expand Down

0 comments on commit 40f1ac7

Please sign in to comment.