CanCanCommunity/cancancan · error · CanCan::Error

The accessible_by call cannot be used with a block 'can' def

Error message

The accessible_by call cannot be used with a block 'can' definition.The SQL cannot be determined for #{action.inspect} #{subject.inspect}

What it means

Model.accessible_by(ability) and load_and_authorize_resource index actions must compile ability rules into SQL. A rule defined with only a block (can :read, Invoice { |i| ... }) cannot be expressed in SQL, so Rules#relevant_rules_for_query (lib/cancan/ability/rules.rb:75) raises CanCan::Error. Note that 'cannot' rules with attributes are silently rejected for queries, but a block rule is fatal.

Source

Thrown at lib/cancan/ability/rules.rb:75

      end

      def relevant_rules_for_match(action, subject)
        relevant_rules(action, subject).each do |rule|
          next unless rule.only_raw_sql?

          raise Error,
                "The can? and cannot? call cannot be used with a raw sql 'can' definition. " \
                "The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
        end
      end

      def relevant_rules_for_query(action, subject)
        rules = relevant_rules(action, subject).reject do |rule|
          # reject 'cannot' rules with attributes when doing queries
          rule.base_behavior == false && rule.attributes.present?
        end
        if rules.any?(&:only_block?)
          raise Error, "The accessible_by call cannot be used with a block 'can' definition." \
            "The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
        end
        rules
      end

      # Optimizes the order of the rules, so that rules with the :all subject are evaluated first.
      def optimize_order!(rules)
        first_can_in_group = -1
        rules.each_with_index do |rule, i|
          (first_can_in_group = -1) && next unless rule.base_behavior
          (first_can_in_group = i) && next if first_can_in_group == -1
          next unless rule.subjects == [:all]

          rules[i] = rules[first_can_in_group]
          rules[first_can_in_group] = rule
          first_can_in_group += 1
        end
      end

View on GitHub (pinned to 8c1bf153a3)

Solutions

  1. Convert the block to a conditions hash when the logic is attribute-based: can :manage, Invoice, amount: 0..100.
  2. Convert to a raw SQL string when it maps to SQL but not to a hash: can :manage, Invoice, 'amount < 100' (then never can? instances).
  3. For genuinely dynamic logic, skip accessible_by: load records with your own scope in the controller and filter in Ruby: @invoices = Invoice.all.select { |i| can?(:read, i) }.
  4. Split abilities per action so only the listing action uses query-friendly conditions.

Example fix

# before
can :read, Invoice { |i| i.amount < 100 }
# InvoicesController#index -> Invoice.accessible_by(current_ability) raises

# after
can :read, Invoice, amount: 0..100.0   # hash compiles to SQL for accessible_by

# or manual filtering for non-SQL logic
def index
  @invoices = Invoice.all.select { |i| can?(:read, i) }
end
Defensive patterns

Strategy: validation

Validate before calling

# before Model.accessible_by(ability)
def queryable?(ability, action, model)
  ability.rules.none? { |rule| rule.only_block? && rule.relevant?(action, model) }
end

raise CanCan::Error, 'block rule blocks accessible_by' unless queryable?(current_ability, :read, Invoice)

Type guard

def block_only_rule?(rule)
  rule.respond_to?(:only_block?) && rule.only_block?
end

Try / catch

begin
  @invoices = Invoice.accessible_by(current_ability)
rescue CanCan::Error
  @invoices = Invoice.all.select { |i| can?(:read, i) } # manual fallback
end

Prevention

When it happens

Trigger: can :manage, Invoice { |i| i.amount < 100 } followed by Invoice.accessible_by(current_ability) or current_ability.model_adapter(Invoice, :read).database_records; any index action of load_and_authorize_resource on a model whose abilities use blocks; controller_specs hitting index with block-based abilities.

Common situations: Delegated/traversal logic in blocks (i.brand.owner_id == user.company_id) for index listings; abilities written block-first because they feel like Ruby, then the dashboard list page raises; upgrading CanCanCan 2 -> 3 where previously-tolerated mixed rules now raise.

Related errors


AI-assisted analysis of CanCanCommunity/cancancan@8c1bf153a3 (2026-08-21). Data as JSON: /api/errors/5252659b1bb9eda6. Report an issue: GitHub.