Every time a request is made to a Rails controller, there can be multiple callbacks involved. The order in which they get called depends on how and when they are added. Let’s take a look!
Filters are simply methods that are run before, after, or around a controller action. They are typically added near the top of a controller by calling (as you might expect) before_action
, after_action
, or around_action
.
There are also nine additional helpers to control when and if they are triggered: prepend_before_action
, prepend_after_action
, prepend_around_action
, append_before_action
, append_after_action
, append_around_action
, skip_before_action
, skip_after_action
, and skip_around_action
.
Note that prior to Rails 4 all of these functions were named *_filter instead of *_action.
To get started, I created a barebones Rails application and added a bunch of filters to the base application controller. I also generated a separate controller with an action that can be executed.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action -> { puts "Calling before_action 1" }
before_action -> { puts "Calling before_action 2" }
around_action do |controller, block|
puts "Calling around_action 1 - before yield"
block.call
puts "Calling around_action 1 - after yield"
end
around_action do |controller, block|
puts "Calling around_action 2 - before yield"
block.call
puts "Calling around_action 2 - after yield"
end
after_action -> { puts "Calling after_action 1" }
after_action -> { puts "Calling after_action 2" }
end
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def test
puts "Executing action"
head :ok
end
end
Hitting the test action results in the following output:
Calling before_action 1
Calling before_action 2
Calling around_action 1 - before yield
Calling around_action 2 - before yield
Executing action
Calling after_action 2
Calling after_action 1
Calling around_action 2 - after yield
Calling around_action 1 - after yield
There are a couple of interesting things to note here. When using before_action
, the filters are called in the order that they are defined (i.e. FIFO). However, when using after_action
, they follow LIFO ordering. Occasionally this catches people off guard; it might be easier to visualize the filters being added right before or after the action as they are defined. The last method, around_action
, effectively follows the same rules as the other two combined.
Let’s add a couple filters to PagesController as well:
class PagesController < ApplicationController
before_action -> { puts "Calling before_action 3" }
after_action -> { puts "Calling after_action 3" }
...
end
Hitting the action again returns the following:
Calling before_action 1
Calling before_action 2
Calling around_action 1 - before yield
Calling around_action 2 - before yield
Calling before_action 3
Executing action
Calling after_action 3
Calling after_action 2
Calling after_action 1
Calling around_action 2 - after yield
Calling around_action 1 - after yield
This shouldn’t be too surprising as the ancestors’ filters were applied first.
Here we modify PagesController again, but this time the filters are prepended instead:
class PagesController < ApplicationController
prepend_before_action -> { puts "Calling before_action 3" }
prepend_after_action -> { puts "Calling after_action 3" }
...
end
This results in:
Calling before_action 3
Calling before_action 1
Calling before_action 2
Calling around_action 1 - before yield
Calling around_action 2 - before yield
Executing action
Calling after_action 2
Calling after_action 1
Calling around_action 2 - after yield
Calling around_action 1 - after yield
Calling after_action 3
The interesting takeaway here is that we can manipulate the order of the entire callback chain from a descendant controller (which makes sense given that the skip_*_action functions exist).
I’ve largely been defining filters inline thus far. It’s usually a bit more common to write them as private methods like below. For this example, I’ll only make use of before_action
to simplify things.
class ApplicationController < ActionController::API
before_action :before_1
before_action :before_2
before_action :before_3
private
def before_1
puts "Calling before_action 1"
end
def before_2
puts "Calling before_action 2"
end
def before_3
puts "Calling before_action 3"
end
end
By giving these filters a name, we can now manipulate them more handily. Let’s say I want before_1
to occur after before_2
but before before_3
. In PagesController, I’ll re-add a couple of filters like so:
class PagesController < ApplicationController
before_action :before_1
before_action :before_3
...
end
Calling before_action 2
Calling before_action 1
Calling before_action 3
How does this work? Adding the same filter overrides any previous definitions; in this case, the ones defined in ApplicationController were removed and new ones were added in PagesController, following the ordering rules noted previously.
Under the hood, filters make use of ActiveSupport::Callbacks to add hooks during the lifecycle of a controller action. This chain can actually be inspected by printing the internal variable __callbacks[:process_action]
(or its alias _process_action_callbacks
).
For the example given under “Prepending Filters” above, the filter chain looks like the following:
# __callbacks[:process_action].map { |c| c.kind }
[
:after, # after_3
:before, # before_3
:before, # before_1
:before, # before_2
:around, # around_1
:around, # around_2
:after, # after_1
:after, # after_2
]
Apart from the first two entries (which were explicitly prepended), the rest of them are in the same order as they are defined. Rails then takes this chain and compiles it into a callback sequence from first to last:
:before
or :after
, it is added to the corresponding arrays:around
, a nested callback sequence is returned and later filters in the chain will be added to it insteadThe callback sequence is then executed with the before filters first, all nested sequences recursively, and then finally the after filters.
Knowing how filters are ordered can be very useful in debugging complex controller logic in your Rails applications. Occasionally there is a need to manipulate the callback chain (such as caching the response of an API endpoint after everything has executed). In general, however, it is best to keep these chains as simple as possible as they can be troublesome to test and debug.