rails: Rails 6.1 Constant autoloading not working in environment configuration files

Steps to reproduce

Referencing an autoloaded constant in an environment configuration (e.g. config/environments/development.rb) results in a NameError (“uninitialized constant”). This worked correctly in Rails 6.0.3.4.

An example repo demonstrating the problem is available here: https://github.com/samstickland/rails-6.1-constant-resolution-issue . Running “rails c” is not possible in this repo with Rails 6.1. The first commit in the repo is the result of “rails new” and the second demonstrates the issue

Expected behavior

The constant should be resolved

Actual behavior

The constant is not resolved.

System configuration

Rails version: 6.1.0

Ruby version: 2.7.2

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 5
  • Comments: 24 (18 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks @fxn! Confirmed that worked, the require inside Application class body.

I don’t know why I didn’t try that first, I thought I did, but clearly not! It is a little bit more straightforward than the config.before_configuration rigamarole.

Hopefully this will be findable and help others trying to move from using auto-loaded classes in configuration/initialization process. The little gotchas made it take more time than I thought it would.

@rromanchuk Let me add that I explained this to help understand the reason why this is the best option. However, I do not assume you had to know this, I believe this is not well documented. Maybe we need a clear contract of what can be done and what cannot be done at each stage of the boot process.

@rromanchuk Ah! In that case the proper solution is to move your code to lib and issue a require call.

When the application boots, config is initialized. But the actual affected component does not operate with config at runtime in general, and if it does, that is private implementation. Normally, config is just a proxy that transports user configuration into internal component state. This is schematically what happens:

Let’s imagine railtie foo provides Foo::Service, which has a class attribute endpoint. As a Ruby programmer developing foo you have

class Foo::Service
  cattr_accessor :endpoint
end

OK. In general, the internal way to represent things is not exposed, the public interface to it goes via config, an indirection, so that railtie has an initializer that does this:

initializer "init.foo" do
  Foo::Service.endpoint = config.foo.endpoint
end

so to speak.

That is tied to the boot process, initializers run only once. If you change config.foo.endpoint on reload via to_prepare, that does not run the initializer again. The endpoint value should not be reloadable, because it is used in a way that makes reloading pointless. See what I mean?

So, better require from lib, so that it is clear that SpecialJsonFormatter is a class whose modification needs a server restart.

@jrochkind before the line with the class keyword, the boot process is mostly loading dependencies. When Rails::Application has been subclassed, you have lib. That means you can simply write

class MyApp::Application < Rails::Application
  # lib is in $LOAD_PATH now.
  require "my_app/helper"
  ...
end

Alternatively, you can also use require_relative, in which case you do not depend on $LOAD_PATH, and can be put at the top:

require_relative "../lib/my_app/helper"

but it is, perhaps, uglier.

The warning shipped with 6.0.0.

In 6.1 you cannot autoload in config/environments/*.rb, that is the first change. You still can autoload in config/initializers/*.rb to ease the transition, but you should not, and it will err in Rails 7 (at least, that is the plan).

We can close this one.

Yes. The reason for this is explained in the warning message.

MyNamespace::MyClass is reloadable because it’s in app/lib. Whatever you do in config/environments/development.rb with MyNamespace::MyClass is not going to be reflected if you edit its implementation, because those initialization files do not run again.

Options:

  1. If you do not need this class to be reloadable, please move it to lib and issue a require call in config/environments/*.rb. If you change its code, a server restart is needed.
  2. If you need it to be reloadable, wrap the code in a to_prepare block, or upgrade to Zeitwerk 2.4.2 and set an on_load callback on Rails.autoloaders.main.

Thanks for the issue, and for the example repo. I believe this was an intentional breaking change for Rails 6.1. This would have worked in Rails 6.0, but it would have output a deprecation warning. I double checked using your example repo back on Rails 6.0.3.4 and I can see this in log/development.log:

DEPRECATION WARNING: Initialization autoloaded the constants MyNamespace and MyNamespace::MyClass.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload MyNamespace, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <main> at rails-6.1-constant-resolution-issue/config/environment.rb:5)

In general I would recommend addressing all deprecation warnings before upgrading to the next version.

Looks like 43863bfc5716d8bd2f5be59920239aee1470465a changed this behavior, so I think I was wrong about it being an intentional breaking change. But it would have eventually been an intentional breaking change 🤣.