rails: Autoloading helpers fails initially in Rails 7.x, but succeeds on the second attempt

Steps to reproduce

When I try loading a Rails Application in Ruby 7.0, the app/helpers autoload path does not appear to be working from my gems engine.

Here’s the script to reproduce the issue NOT working under 7.0:

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "sitepress", github: "sitepress/sitepress", branch: "rails-7"
  gem "rails", github: "rails/rails", branch: "main"
end

require "sitepress/server"  # Load all the stuff needed setup the configuration below.

# Setup defaults for stand-alone Sitepress server in the current path. This
# can, and should, be over-ridden by the end-user in the `config/site.rb` file.
Sitepress.configure do |config|
  config.routes = false
  config.site = Sitepress::Site.new root_path: "."
end

# Create the module that should autoload without an issues.
require 'fileutils'
FileUtils.mkdir_p 'helpers'
File.write "helpers/foo_helper.rb", "module FooHelper; end;"

# Boot the Rails app
app = Sitepress::Server
app.initialize!

puts "Loading ::SiteController fails in Rails 7.0"
# See https://github.com/sitepress/sitepress/blob/rails-7/sitepress-rails/lib/sitepress/engine.rb#L15 for helper path configuration
# See https://github.com/sitepress/sitepress/blob/rails-7/sitepress-server/lib/sitepress/server.rb for the configuration of the Rails app this boots
p ::SiteController # This will raise an exception

Now for code that shows this WORKING under Rails 6:

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "sitepress", github: "sitepress/sitepress", branch: "rails-7"
  gem "rails"
end

require "sitepress/server"  # Load all the stuff needed setup the configuration below.

# Setup defaults for stand-alone Sitepress server in the current path. This
# can, and should, be over-ridden by the end-user in the `config/site.rb` file.
Sitepress.configure do |config|
  config.routes = false
  config.site = Sitepress::Site.new root_path: "."
end

# Create the module that should autoload without an issues.
require 'fileutils'
FileUtils.mkdir_p 'helpers'
File.write "helpers/foo_helper.rb", "module FooHelper; end;"

# Boot the Rails app
app = Sitepress::Server
app.initialize!

puts "Loading ::SiteController succeeds in Rails 6.x"
p ::SiteController # This will not raise an exception

Finally, in a third example under Rails 7.0, I demonstrate that this is WORKING when I attempt to load the constant a second time:

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "sitepress", github: "sitepress/sitepress", branch: "rails-7"
  gem "rails", github: "rails/rails", branch: "main"
end

# require "sitepress/boot"
require "sitepress/server"  # Load all the stuff needed setup the configuration below.

# Setup defaults for stand-alone Sitepress server in the current path. This
# can, and should, be over-ridden by the end-user in the `config/site.rb` file.
Sitepress.configure do |config|
  config.routes = false
  config.site = Sitepress::Site.new root_path: "."
end

# Create the module that should autoload without an issues.
require 'fileutils'
FileUtils.mkdir_p 'helpers'
File.write "helpers/foo_helper.rb", "module FooHelper; end;"

# Boot the Rails app
app = Sitepress::Server
app.initialize!

puts "Loading ::SiteController fails in Rails 7.0 on the first attempt"
begin
  p ::SiteController
rescue => e
  puts "Failed with #{e.inspect}"
end

puts "Loading ::SiteController succeeds in Rails 7.0 on the second attempt"
p ::SiteController

The following source files may be helpful to help diagnose the issue:

Expected behavior

I expect the FooHelper to be resolved in Rails 7.x, as it does in Rails 6.x. Here’s what a succesful script run looks like from above:

SiteController

Actual behavior

The FooHelper module is not resolved when Rails attempts to find it. Here’s what the error looks like from the first Rails 7.x script above:

/Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/activesupport/lib/active_support/inflector/methods.rb:280:in `constantize': uninitialized constant FooHelper (NameError)
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/activesupport/lib/active_support/core_ext/string/inflections.rb:74:in `constantize'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/abstract_controller/helpers.rb:177:in `block in modules_for_helpers'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/abstract_controller/helpers.rb:170:in `map!'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/abstract_controller/helpers.rb:170:in `modules_for_helpers'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/action_controller/metal/helpers.rb:104:in `modules_for_helpers'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/abstract_controller/helpers.rb:148:in `helper'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/rails-a692e63bf400/actionpack/lib/action_controller/railties/helpers.rb:19:in `inherited'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/sitepress-cd078ab7255c/sitepress-server/rails/app/controllers/application_controller.rb:1:in `<top (required)>'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/zeitwerk-2.5.0.beta3/lib/zeitwerk/kernel.rb:27:in `require'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/zeitwerk-2.5.0.beta3/lib/zeitwerk/kernel.rb:27:in `require'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/bundler/gems/sitepress-cd078ab7255c/sitepress-server/rails/app/controllers/site_controller.rb:1:in `<top (required)>'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/zeitwerk-2.5.0.beta3/lib/zeitwerk/kernel.rb:27:in `require'
	from /Users/bradgessler/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/zeitwerk-2.5.0.beta3/lib/zeitwerk/kernel.rb:27:in `require'
	from bug.rb:40:in `<main>'

System configuration

Rails version: 7.x

Ruby version: 3.x

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks for patching it and a huge thanks for maintaining Zeitwerk and all of your contributions to Rails.

Fantastic @bradgessler! We have it narrowed down. I’ll think about it and will write back.

I’ve played around for a few minutes. Some details I see:

First, let’s eliminate case (3) from the equation. In Ruby, you know that if you have this

# foo.rb
class Foo
  raise

  def x
  end
end

and then

begin
  require "foo"
rescue
  # ignored
end

Foo

That works, because the Foo constant was actually defined. You are not loading again and succeeding, it was the first load the succeeded partially. Of course, instances of Foo won’t respond to #x, that is expected.

That is in essence what is happening in case (3).

There’s one difference between case (1) and case (2), if you

pp ActiveSupport::Dependencies.autoload_paths

after initialization, you’ll see tmp/helpers is present in (2), and missing in (1).

I don’t know why is that yet, just reached this point by now, but looks like the problem is the configurations, for some reason, are different between both versions.

I’ll try to dig into it, thanks a lot for testing the alpha ❤️.