Ordering of Filters in Rails Controllers

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!

What is a Filter?

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.

A Basic Example

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.

Inheriting Filters

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.

Prepending Filters

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).

Reordering Individual Filters

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.

Demystifying the Magic

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:

  • If the filter is of type :before or :after, it is added to the corresponding arrays
  • If the filter is of type :around, a nested callback sequence is returned and later filters in the chain will be added to it instead
Callback Sequence

The callback sequence is then executed with the before filters first, all nested sequences recursively, and then finally the after filters.

Final Remarks

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.

January 27, 2020