`before_action` / `skip_before_action` with `only` / `except` / `if` / `unless` - nonintuitive and inconsistent

The docs for _normalize_callback_options make it clear that these controller hooks will behave unexpectedly when combining if / unless with only / except:

only: :index, if: -> { true } # the :if option will be ignored.
except: :index, if: -> { true } # the :except option will be ignored.

In practice, I don’t think many people will make it so far into the docs to understand this, until they encounter a bug with their code or inexplicable failing test. It’s also inconsistent:

# seems to work fine
# Will only run the action if certain_endpoint is hit and some_condition? is true
before_action :do_thing, only: [:certain_endpoint], if: :some_condition?
# does not work!
# Will NOT apply the only: [:certain_endpoint] filter
skip_before_action :standard_auth, only: [:certain_endpoint], if: :passes_special_auth?

If this is intended behavior, I think it’s bad design. The fact that only compiles into if is an implementation detail leaking into the interface. I imagine that the implementation could convert only into an if without replacing the if that the user specifies (e.g. it could combine both conditions).

1 Like

I think the documentation is off. When I tested with the following code, the except option was not ignored.

class C1Controller < ApplicationController
  before_action :cb_a, except: :index, if: -> { true }

  def cb_a
    track("A")
  end
  
  # /c1/index
  def index
    render_ran
  end
  
  # /c1/show
  def show
    render_ran
  end

  private
  def track(name)
    (@_ran ||= []) << name
  end

  def render_ran
    render json: { ran: (@_ran || []) }
  end
end
curl -s localhost:3000/c1/index
{"ran":[]}
curl -s localhost:3000/c1/show 
{"ran":["A"]}
1 Like