Skip to content

Fix NoMethodError on Rails 8.1 and move the ActiveAdmin 4.0 leg to it - #23

Merged
Fivell merged 3 commits into
mainfrom
fix/rails-8-1-railtie-require
Oct 1, 2026
Merged

Fivell merged 3 commits into
mainfrom
fix/rails-8-1-railtie-require

Conversation

@Fivell

@Fivell Fivell commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

What

require "activeadmin-oidc" raises NoMethodError on Rails 8.1. This fixes it
with a one-line require, then moves the ActiveAdmin 4.0 CI leg onto Rails 8.1
and drops the json < 3 workaround that leg no longer needs.

This is a user-facing bug, not a CI detail. The gemspec allows
rails >= 7.2, < 9, so Rails 8.1 is inside the declared support range and
every host app on Rails 8.1 crashes on require today.

Root cause

railties 8.1 added delegate_missing_to :@collection to the body of
Rails::Initializable::Collection, but rails/initializable.rb only requires
"tsort" — it never requires the ActiveSupport core ext that defines
delegate_missing_to. And rails/railtie.rb requires rails/initializable
before any active_support/core_ext.

This gem requires "rails/railtie" directly (deliberately, to register
omniauth-rails_csrf_protection's railtie for host apps without loading all of
Rails), so it is the first thing to load that file and it trips over the missing
method.

Reproduction

No ActiveAdmin, no this gem — just railties 8.1.4:

$ bundle exec ruby -e 'require "rails/railtie"'
railties-8.1.4/lib/rails/initializable.rb:41:in '<class:Collection>': undefined method 'delegate_missing_to' for class Rails::Initializable::Collection (NoMethodError)

      delegate_missing_to :@collection
      ^^^^^^^^^^^^^^^^^^^

Require the one core ext first and it loads fine:

$ bundle exec ruby -e 'require "active_support/core_ext/module/delegation"; require "rails/railtie"; puts "loaded Rails::Railtie OK"'
loaded Rails::Railtie OK

railties 8.0.5.1 is unaffected — its initializable.rb does not use
delegate_missing_to at all — which is why this only appeared when the 8.1
series shipped.

Upstream

The actual defect is in railties: a file that calls delegate_missing_to should
require active_support/core_ext/module/delegation itself. This should be
reported upstream to rails/rails
— the workaround here protects this gem's
users now, but any library that requires rails/railtie before ActiveSupport
hits the same wall, so it deserves a fix in railties rather than N copies of
this require across the ecosystem.

The fix

lib/activeadmin-oidc.rb, immediately before the existing require "rails/railtie":

require "active_support/core_ext/module/delegation"

Why the minimal core ext, and not require "rails"

The existing comment block says the gem pulls in the minimal Railtie base class
rather than the full Rails stack, so the require stays safe in contexts where
Rails has not been initialized. Requiring "rails" or "active_support/rails"
would "fix" the NoMethodError by loading far more than this gem needs and would
throw away that property. The single core ext railties forgot keeps the original
intent exactly:

$ bundle exec ruby -e 'require "activeadmin-oidc"; puts "OK: #{ActiveAdmin::Oidc::VERSION}; Railtie=#{defined?(Rails::Railtie)}; full Rails loaded? #{defined?(Rails::Application) ? "yes" : "no"}"'
OK: 2.2.1; Railtie=constant; full Rails loaded? no

Rails::Railtie is defined, Rails::Application is still not loaded. The comment
block above the require has been extended to explain why the core ext has to come
first, so nobody "tidies" it away.

Regression test

None added — the existing suites already are one. All four suites require the
gem through spec/spec_helper.rb, so on Rails 8.1 without this change every one
of them dies before a single example runs:

An error occurred while loading spec_helper.
Failure/Error: require "rails/railtie"
NoMethodError: undefined method 'delegate_missing_to' for class Rails::Initializable::Collection
# ./lib/activeadmin-oidc.rb:21:in '<top (required)>'
# ./spec/spec_helper.rb:5:in '<top (required)>'

Confirmed for rake spec, spec:engine, spec:isolated and spec:root. Once
the ActiveAdmin 4.0 gemfile is on Rails 8.1 (commit 2), CI itself is the guard —
a revert of commit 1 turns all three 8.1 legs red immediately. Adding a separate
clean-process load example would be redundant.

Gemfile change

gemfile before after
gemfiles/activeadmin_4.0.gemfile rails "~> 8.0.0" rails "~> 8.1.0"
gemfiles/activeadmin_3.5.gemfile rails "~> 8.0.0" unchanged

Rails 8.0 goes EOL 2026-11-07; 8.1 is supported until 2027-10-10
(endoflife.date/rails), so this gives the repo a
long-lived Rails leg. With the 3.5 leg staying on 8.0, both currently supported
Rails series are covered.
The pin stays three-segment: ~> 8.1 would mean
>= 8.1, < 9.0 and would drift onto 8.2 once it ships, so the leg would stop
testing the version it names.

This branch is rebased on top of #22 (now merged), so it also picks up that
PR's Ruby matrix (['3.3', '3.4', '4.0']) and its rails "~> 8.0.0" pin on the
3.5 leg. #22 had added a comment above the 4.0 gemfile's rails pin naming this
very blocker; since the blocker is fixed here, that comment is deleted in commit
2
rather than left to rot.

json < 3 verdict: dropped from the 4.0 gemfile only

json 3.0 removed the quirks_mode keyword that ActiveSupport::JSON passed to
JSON.parse/JSON.generate. Checked the actual gem sources:

activesupport passes quirks_mode?
7.2.3 yes — lib/active_support/json/{decoding,encoding}.rb
8.0.5.1 yes — same two files
8.1.4 no — no occurrence anywhere in lib/

Verified rather than assumed. With the pin removed and the lockfile deleted
(CI has no lockfile, which is the whole premise of the pin), bundler resolves
json 3.0.2 against Rails 8.1.4 and all four suites pass with zero
unknown keyword: quirks_mode anywhere.

gemfiles/activeadmin_3.5.gemfile keeps its pin and its comment — that leg
runs activesupport 8.0, which still passes the keyword. Measured: remove the pin
there and, on Rails 8.0.5.1, bundler resolves json 3.0.2 and the default suite
gives 59 failures / 90 quirks_mode mentions. The pin is still load-bearing
on that leg.

Worth flagging for anyone verifying locally: a stale gemfiles/*.gemfile.lock
silently masks this. My first attempt resolved json 2.21.2 and looked like the
pin was unnecessary; it was the leftover lock, not the gemfile. Delete the lock
to reproduce what CI does.

Release

This warrants a patch release. Until it ships, gem "activeadmin-oidc" is
broken for every host app on Rails 8.1, and the gemspec advertises support for it
(rails >= 7.2, < 9). I have deliberately not bumped
lib/activeadmin/oidc/version.rb (currently 2.2.1) — the release decision and
the version number are the maintainer's.

Verification

CI=true, all four suites run exactly as the Rakefile invokes them, lockfiles
deleted first so resolution matches CI, explicit --seed 1:

ruby gemfile resolved spec engine isolated root
3.4.10 activeadmin_3.5 rails 8.0.5.1, json 2.21.2 161, 0 fail, 1 pend 7, 0 8, 0 6, 0
3.4.10 activeadmin_4.0 rails 8.1.4, json 3.0.2 161, 0 fail, 8 pend 7, 0 8, 0 6, 0
4.0.6 activeadmin_3.5 rails 8.0.5.1, json 2.21.2 161, 0 fail, 1 pend 7, 0 8, 0 6, 0
4.0.6 activeadmin_4.0 rails 8.1.4, json 3.0.2 161, 0 fail, 8 pend 7, 0 8, 0 6, 0
3.3.12 activeadmin_3.5 rails 8.0.5.1, json 2.21.2 161, 0 fail, 1 pend 7, 0 8, 0 6, 0
3.3.12 activeadmin_4.0 rails 8.1.4, json 3.0.2 161, 0 fail, 8 pend 7, 0 8, 0 6, 0

(Re-run after rebasing onto merged #22, so the 3.5 legs here are on Rails 8.0.)

bundle install exited 0 for all six. Ruby 3.3 checked on 3.3.12, never
3.3.0 (3.3.0 is the one release with the anonymous-parameter parser bug, which
ruby/setup-ruby never installs for ruby-version: '3.3').

Seed observations — the pre-existing flake is unchanged by Rails 8.1

spec/spec_helper.rb sets config.order = :random with a fresh seed per run, and
the default suite has a pre-existing order dependency on Rails 8 (documented in
#22, present on main today). I re-measured it on Rails 8.1 to see whether the
move changes anything. activeadmin_4.0 gemfile, Ruby 3.4.10:

seed Rails 8.0 (main) Rails 8.1 (this branch)
1–5, 7–10 0 failures 0 failures
6 1 failure 1 failure
6233 1 failure 1 failure
21181 2 failures 2 failures
35323 0 failures 0 failures

Identical — same seeds, same failure counts, same two examples:

  • spec/unit/configuration_spec.rb:203 — "#login_submit_path points at the
    OmniAuth entry point when stub login is off"
    (builds its expectation from the
    global OmniAuth.config.path_prefix, which Devise overwrites at route-draw
    time; under Rails 8 lazy route loading the value depends on spec order)
  • spec/requests/stub_login_spec.rb:224 — "404s when the flag is flipped off
    after the route was drawn"

So moving to Rails 8.1 neither fixes nor worsens the flake — roughly 1 seed in
10, on 8.0 and 8.1 alike. I have not touched those specs here; it is unrelated
to this bug and belongs in its own PR.

It already bit this PR's first CI run — and it bites main too

CI on this branch came back 5 pass / 1 fail on the first attempt: Ruby 3.3 /
activeadmin_3.5.gemfile
, seed 18410, 9 failures, all
ActionController::RoutingError in logout_spec.rb, boot_spec.rb and
omniauth_callback_spec.rb — a bigger cluster than the two examples above, but
the same route-draw order dependency underneath. Re-running that one job (new
seed, same commit) turned it green, so the final state is 6/6 pass.

It is not caused by this PR. This PR does not touch the 3.5 gemfile at all,
and the failure reproduces identically on main without the fix commit:

tree ruby gemfile seed 18410
this branch 3.3.12 activeadmin_3.5 (Rails 8.0) 9 failures
merged main (2f640aa), no fix commit 3.3.12 activeadmin_3.5 (Rails 8.0) 9 failures — same 9 examples

Independently: main's own post-merge CI run for 2f640aa is red, 2 of 6 legs
(Ruby 4.0 / activeadmin_3.5, Ruby 4.0 / activeadmin_4.0), seed 62645, failing
on spec/unit/configuration_spec.rb:203 — the same flaky example documented in
#22. So the trunk is already intermittently red on its own.

This is the exposure widening that #22 flagged, now visible in practice. Fixing
these specs is the most valuable next piece of work in this repo
— it is
currently impossible to tell a real regression from a seed at a glance, which is
exactly the condition in which a real one slips through. Suggested approach for
that PR: stop asserting against the mutable OmniAuth.config.path_prefix global,
and give the route-redrawing examples proper setup/teardown so they cannot leak
route state into whatever runs next.

🤖 Generated with Claude Code

Fivell added 3 commits October 1, 2026 14:25
On Rails 8.1 requiring this gem raises

  NoMethodError: undefined method 'delegate_missing_to'
                 for class Rails::Initializable::Collection

railties 8.1 calls delegate_missing_to in the body of
Rails::Initializable::Collection, but rails/initializable.rb only
requires "tsort" and rails/railtie.rb requires rails/initializable
before any ActiveSupport core ext. This gem requires "rails/railtie"
directly, so it is the first thing to load that file and it trips over
the missing method.

Reproducible with no ActiveAdmin in the picture:

  # railties 8.1.4
  ruby -e 'require "rails/railtie"'                       -> NoMethodError
  ruby -e 'require "active_support/core_ext/module/delegation"; require "rails/railtie"'
                                                          -> OK
  # railties 8.0.5.1
  ruby -e 'require "rails/railtie"'                       -> OK

This is a user-facing bug, not a CI detail: the gemspec allows
rails >= 7.2, < 9, so every host app on Rails 8.1 crashes on require.

Fixed by requiring the single core ext railties forgot, which preserves
the existing intent of loading only the minimal Railtie base class
rather than all of Rails -- requiring "rails" or "active_support/rails"
would both pull in far more.

No new spec: all four suites require the gem via spec_helper, so each
one already fails outright on Rails 8.1 without this change.
Now that the gem loads on Rails 8.1 the AA 4.0 leg can test it. Rails
8.0 goes EOL on 2026-11-07 (endoflife.date/rails); 8.1 is supported
until 2027-10-10, so this gives the repo a long-lived Rails leg.

gemfiles/activeadmin_3.5.gemfile deliberately stays on "~> 8.0.0" so
both currently supported Rails series are still covered.

The pin stays three-segment: a two-segment "~> 8.1" would mean
>= 8.1, < 9.0 and would drift onto 8.2 once that ships, so the leg
would stop testing the version it names.

Note on merge order: PR #22 adds a comment above this pin naming the
Rails 8.1 blocker that this branch fixes. This branch is cut from main,
where that comment does not exist yet. If #22 merges first, rebase and
delete that now-stale comment.
The pin worked around json 3.0 removing the `quirks_mode` keyword that
ActiveSupport::JSON passed to JSON.parse/JSON.generate. activesupport
8.1 no longer passes it -- `quirks_mode` does not appear anywhere in
activesupport 8.1.4's lib/, while 7.2.3 and 8.0.5.1 both pass it in
lib/active_support/json/{decoding,encoding}.rb. Now that this gemfile
is on Rails 8.1 the workaround is dead weight.

Verified rather than assumed: with the pin removed and no lockfile
(as on CI) bundler resolves json 3.0.2, and all four suites pass on
Ruby 3.4.10 -- 161, 7, 8 and 6 examples, 0 failures, no
"unknown keyword: quirks_mode" anywhere.

gemfiles/activeadmin_3.5.gemfile keeps its pin and its comment: that
leg runs an older activesupport, which still passes the keyword.
Removing the pin there resolves json 3.0.2 and produces 59 failures.
@Fivell
Fivell force-pushed the fix/rails-8-1-railtie-require branch from 7cf078f to c651634 Compare October 1, 2026 12:28
@Fivell
Fivell merged commit 49275c5 into main Oct 1, 2026
11 of 12 checks passed
@senid231 senid231 mentioned this pull request Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant