Request Life Cycle
Praxis processes each incoming request by funneling them through a pipeline of stages. Stages are execution points during the servicing of a request, which could themselves contain a set of sub-stages. Each registered stage has a unique name and is connected to the other stages in a well-known order.
Praxis allows applications to ‘hook into’ any of those existing stages through
after callbacks and provides facilities to create or alter the
pipeline. Any stage or its hooks are able to abort the pipeline processing early,
and shortcut directly to sending a response to the client.
Note: while stages in the request lifecycle might behave similarly to bootstrap stages, they perform a different role. Request life cycle stages define the processing path for every incoming request, while bootstrapping stages define the execution order when the application first boots.
Praxis comes out of the box with the following stage pipeline:
The first of these stages in the pipeline will only be invoked after the routing has been processed for the incoming request, and the appropriate controller and action has been identified. This means that currently, there is no way to affect the request routing dynamically.
Request Loading Stage (
The request loading stage is used to retrieve all the necessary information from the incoming HTTP request so that it is ready for processing.
- parsing the parameters from the URI captures
- retrieving any parameters from query string
- retrieving the paylod contents
- retrieving the incoming headers
All of these, without performing any parsing or type coercion: simply gathering the low-level arguments in one place.
Note: Strictly speaking retrieving the query string params involves some form-encoding parsing, but it still does not involve any type coercion.
Validation Stage (
During the Validate stage, Praxis will gather the raw data retrieved from the loading stage and will:
- load them (read: coerce if necessary) into the proper structures as defined in your resource definitions for the action that this request is serving.
- validate them based on the same resource definitions.
These two tasks are done for data corresponding to headers, parameters and
payload. Any errors while loading the data into the right types, or validating
their integrity will cause Praxis to abort the pipeline (by shortcutting to the
:response stage) and return an error to the end user indicating the exact
problem (or problems) that were encountered about the incoming data.
In particular, the Validation stage is composed of two sub-stages.
Validate headers and params stage (
First, this stage will load the headers and the parameters. If all loaded fine they will both be validated in the same order.
Note that separating headers+params from the payload validation makes it not possible to have conditional requirements for headers or parameters that depend on payload values.
Validate payload stage (
Then this stage will load the incoming payload and then validate its integrity after that.
Note that because the payload is validated after the headers and params, it is
completely possible to use conditional requirements based on their (loaded)
values. That is using the
:required_if option on payload attributes.
Once the incoming request has been loaded and validated, the next step is to deliver it to the controller action that will service it.
The request enters the Action stage at the point when the controller action is invoked. This is the stage where the application will do its logic.
Response Stage (
The Response stage is going to always be invoked, even when any of the previous pipeline stages have decided to shortcut the cycle. This is done to give the application a chance to catch and possibly modify the logic involved in returning the response to the user, even when it is an error response. In the normal, non-error case however, the Response stage is entered when the control returns from the controller action stage.
The responsibility of this action is to inspect the returned value (usually a response instance) and perform the necessary steps to unpack it and send it back to the client.
Note that any of the around, before or after filters in any other stage will immediately
shortcut the pipeline to this
:response stage. Therefore, you should never assume
that your filters around your actions are always going to be successfully executed
by the time the
:response stage code is invoked. For example, if there is a
filter that sets the current user into the request object, do not assume that this
user will be correctly set when the response stage executes, as previous
filter might have shortcut the cycle first.
Hooking Into the Request Life Cycle
There are three types of hooks you can use to run a block of code during the
life cycle of a request. You can register a callback to be run either
around any of the available stages.
Installing callbacks is done directly from your controller. Just use the class
around that comes with the Praxis::Callback
concerns(already included by the Praxis::Controller module). Each of these methods take
the name of the stage to hook into, an optional list of options, and a callback block.
The name of the stage can be any of the ones described above:
:validate, :headers_and_params or
:validate, :payload to tap into a sub-stage only ),
The only option supported at the time of this writing is
allows the caller to restrict the callback to be applied only to a set of named actions.
actions option is logically equivalent to passing every possible action in
your controller. More options for callbacks might be introduced in the future.
To install your hook for a substage, add the second stage name
after the first (i.e.
after :validate, :payload ... ). If you completely omit the stage name, Praxis will default to
action stage because that’s the most common use case.
Here are some examples of how to register callbacks:
Technically speaking there is not much difference between
after :validate and
since they are subsequent stages. Semantically, however, they are different as all the
after :validate callbacks will be executed before any of the
before :action ones.
So you should really register the callback based on what stage you depend on, and not on neighboring stages. Otherwise, your code might stop functioning when the pipeline order is changed.
There is, however, an important difference beween an
after :action callback, and a
before :request one. That is because the
:request stage is always invoked regardless of
errors in the previous stages. Therefore
after :action will be always skipped on previous stage
before :request will always be invoked regardless of shortcuts (assuming that no other
before :request callbacks fail before).
There is currently no mechanism to order the callbacks for a given stage. They will be executed
in the order that they were registered. Also, there is currently no way to install callbacks around the complete request lifecycle, for example, to install an
around callback wrapping all of the
individual request stages. Both of these mechanisms can be added if the need arises.
To achieve something similar to a request
around filter, use the builtin middleware registration that the Application provides
Shortcutting the request processing
Any of the registered callback blocks (or the core stage execution code itself) can return a
Praxis::Response-derived object to signal the interruption of the request lifecycle processing. Anything else that the block returns (i.e., nil or any other value) will be ignored and assumed that it signals that the processing should continue.
before callback returns a response, the system will immediately stop processing any further
callbacks of any kind, and shortcut the execution to the
:response stage. This means that (with
the exception of the
- no other
beforecallbacks in the chain will be executed
- none of the
aroundfilters will be executed
- the action won’t be invoked
- none of the
aftercallbacks will be run either
after callback returns a response, no further
after callbacks will be executed either.
Also note that the
around callbacks are always started in after the
before ones, since they wrap the processing of the controller