Rails filter chains are built on
ActiveSupport::Callbacks. They offer a convenient structure for executing shared logic before, after or around controller actions. For example, in our
AdminFooController below, the
before_action method call is used to set up basic authorization before each controller action:
In this case the user will get an unauthorized response unless
class AdminFooController before_action :ensure_admin def index end … private def ensure_admin return if current_user.is_admin? render "shared/error", status: :unauthorized end end
current_useris an admin. In our scenario we use the
before_actionto register a callback (namely
ensure_admin), and this callback halts the request cycle when the user is not an admin, by calling
However, other callbacks exist which can be useful in other scenarios. Contrast the
after_action which can be used to register a callback that will fire after the controller action has successfully executed, and will have access to the
Common examples of logic that can be found in controller filters include authentication and authorization checks, application logging and setting response headers.
Clearly controller filters are a very useful concept, so what problems can we encounter?
Well, there are a few things to be aware of. The temptation to cram too much logic into filters is definitely a general anti-pattern which can lead to maintainability problems. But the article today looks at a specific case which caused me some degree of confusion, so I thought it would be worth sharing.
We introduce a very basic authorization module
FooAuthorizable to demonstrate the effect.
The purpose of this controller mixin is to authorize each controller action by checking that the
is an even number. If
params[:id] is not even then a
FooAuthorizationError will be raised
and the user will receive an error response.
module FooAuthorizable extend ActiveSupport::Concern class FooAuthorizationError < StandardError; end included do before_action :authorize rescue_from FooAuthorizationError do |exception| render "shared/error", status: :forbidden end end def authorize # Dummy authorization, suppose user can only access even IDs raise FooAuthorizationError unless params[:id]&.to_i%2 == 0 end end
We can see this module employed in the
This basic controller contains two actions,
class FoosController < ApplicationController include FooAuthorizable def show end def edit end end
edit. Thanks to the logic included by the
FooAuthorizablemodule we will get successful or unsuccessful responses when we visit different URLs based on whether the
idis even or odd, respectively. For example:
/foos/1024 # Successful response /foos/1025 # Unsuccessful response /foos/1024/edit # Successful response /foos/1025/edit # Unsuccessful response
Now imagine that our project evolves further, and we have some
Foo instances that we want to expose to everyone, like public demos. In fact, for any
id<10 we want anyone to be able to view it. So we add new
before_action as follows:
Our intention here is to apply different filtering logic, only when the action is
class FoosController < ApplicationController include FooAuthorizable before_action :authorize, unless: :public_foo?, only: :show def show end def edit end private def public_foo? params[:id]&.to_i < 10 end end
showand only when
id<10. And our initial check suggests that this seems to work well; we try to view the different
Fooinstances and we see the behaviour we expect:
However, when we test the
/foos/3 # Successful response /foos/4 # Successful response /foos/5 # Successful response /foos/1023 # Unsuccessful response /foos/1024 # Successful response /foos/1025 # Unsuccessful response
editURLs we are alarmed to find that it looks like all of our
Fooinstances are editable! Yikes:
So what has happened here? We can see the following note in the
/foos/3 # Successful response /foos/4 # Successful response /foos/5 # Successful response /foos/1023 # Successful response /foos/1024 # Successful response /foos/1025 # Successful response
actionpacksource code for
NOTE: Calling the same callback multiple times will overwrite previous callback definitionsIt turns out that, using the same symbol (
:authorize) in our new
before_actionhas actually overwritten the original filter. It doesn't matter that our new filter has applied the logic only to the
showaction. Whilst these options are applied at runtime, they do not contribute to the identity of the filter. Our new filter on the
showaction overwrites the previous filter which covered all actions, rendering our
editaction completely unprotected.
Once we identify this problem, how can we solve it?
There are probably any number of ways to solve the problem and achieve the behaviour we were hoping to model. Here is one option:
In this case we have employed the
class FoosController < ApplicationController include FooAuthorizable skip_before_action :authorize, only: :show before_action :authorize_for_show, only: :show def show end def edit end private def authorize_for_show authorize unless public_foo? end def public_foo? params[:id]&.to_i < 10 end end
skip_before_actionto disable the existing filter, only for the
show. And we then add a new filter method,
authorize_for_show. This is applied only to the
showaction and its unique name ensures that it does not clobber the existing
Filters can be applied to Rails controller actions to great effect, for DRYing up cross-cutting concerns like authorization and logging. However, when multiple filters are applied via mixins and inheritance it is easy to construct a tangled mess. This article looked at a case where adding a new
before_action filter, targetting a specific action, unintentionally clobbered the existing filter for all other actions.