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
before
and 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 (:load_request
)
The request loading stage is used to retrieve all the necessary information from the incoming HTTP request so that it is ready for processing.
This involves:
- 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 (:validate
)
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 (:headers_and_params
)
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 (:payload
)
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.
Action stage(:action
)
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 (:response
)
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 before :action
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 before :action
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 before
,
after
or around
any of the available stages.
Installing callbacks is done directly from your controller. Just use the class
DSL methods before
, after
or 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: :load_request
, :validate
(including :validate, :headers_and_params
or :validate, :payload
to tap into a sub-stage only ), :action
or :response
.
The only option supported at the time of this writing is :actions
, which
allows the caller to restrict the callback to be applied only to a set of named actions.
Passing no 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
the 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 before :action
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
shortcuts, while 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.
If a 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 :request
stage):
- no other
before
callbacks in the chain will be executed - none of the
around
filters will be executed - the action won’t be invoked
- none of the
after
callbacks will be run either
If an 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 action
.