Skip to content

NameError: uninitialized constant ViewComponent::SystemTestControllerNefariousPathError on boot when eager_load = true (regression in 4.9.0) #2629

@kazu-2020

Description

@kazu-2020

Steps to reproduce

  1. Add gem "view_component", "4.9.0" to a Rails 8 application.
  2. Ensure config.eager_load = true in the test environment used for booting
# config/environments/test.rb

# Eager loading loads your entire application. When running a single test locally,
# this is usually not necessary, and can slow down your test suite. However, it's
# recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present?
  1. run RAILS_ENV=test bin/rails zeitwerk:check or RAILS_ENV=test CI=1 bin/rails test
# RAILS_ENV=test bin/rails zeitwerk:check
Hold on, I am eager loading the application.
bin/rails aborted!
NameError: uninitialized constant ViewComponent::SystemTestControllerNefariousPathError (NameError)

    rescue_from ViewComponent::SystemTestControllerNefariousPathError, with: :render_not_found
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expected behavior

Rails.application.initialize! succeeds and the ViewComponentsSystemTestController class body loads cleanly during eager loading.

Actual behavior

Booting the application raises NameError: uninitialized constant ViewComponent::SystemTestControllerNefariousPathError from inside the gem itself, before any host application code executes.

The 4.9.0 changes to app/controllers/view_components_system_test_controller.rb ) added a class-body reference:

rescue_from ViewComponent::SystemTestControllerNefariousPathError, with: :render_not_found

commit: da99314

That constant is defined in lib/view_component/errors.rb, which is required only via lib/view_component/base.rb:9. ViewComponent::Base itself is registered as autoload in lib/view_component.rb, so errors.rb is not loaded eagerly at gem-require time. When Zeitwerk eager-loads the host application, it descends into the engine's app/controllers/ and loads view_components_system_test_controller.rb before anything has triggered loading ViewComponent::Base — so the constant lookup at the rescue_from line fails.

In 4.8.0 the same error class was only referenced inside a method body (raise ViewComponent::SystemTestControllerNefariousPathError unless ...), so resolution was deferred until request time. Moving the reference into the class body in 4.9.0 introduced the regression.

The smallest, most local fix is to declare the dependency at the top of the controller, mirroring the existing pattern in lib/view_component/base.rb:9:

--- a/app/controllers/view_components_system_test_controller.rb
+++ b/app/controllers/view_components_system_test_controller.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true

+require "view_component/errors"
+
 class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
   if Rails.env.test?
     before_action :validate_file_path

I verified locally that this clears the boot failure. A more structural alternative would be to require "view_component/errors" from lib/view_component/engine.rb so that all ViewComponent::*Error constants are resolvable from any class body in the gem. Happy to send a PR for whichever approach you prefer! 👍🏻

Backtrace
view_component-4.9.0/app/controllers/view_components_system_test_controller.rb:11:in '<class:ViewComponentsSystemTestController>': uninitialized constant ViewComponent::SystemTestControllerNefariousPathError (NameError)

    rescue_from ViewComponent::SystemTestControllerNefariousPathError, with: :render_not_found
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    from view_component-4.9.0/app/controllers/view_components_system_test_controller.rb:3:in '<main>'
    from ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'
    from bootsnap-1.23.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:33:in 'Kernel#require'
    from zeitwerk-2.7.5/lib/zeitwerk/core_ext/kernel.rb:26:in 'Kernel#require'
    from zeitwerk-2.7.5/lib/zeitwerk/cref.rb:62:in 'Module#const_get'
    from zeitwerk-2.7.5/lib/zeitwerk/cref.rb:62:in 'Zeitwerk::Cref#get'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb:171:in 'block in Zeitwerk::Loader::EagerLoad#actual_eager_load_dir'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb:32:in 'block in Zeitwerk::Loader::FileSystem#ls'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb:26:in 'Array#each'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/file_system.rb:26:in 'Zeitwerk::Loader::FileSystem#ls'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb:166:in 'Zeitwerk::Loader::EagerLoad#actual_eager_load_dir'
    from zeitwerk-2.7.5/lib/zeitwerk/loader/eager_load.rb:17:in 'block (2 levels) in Zeitwerk::Loader::EagerLoad#eager_load'
    ...
    from railties-8.1.3/lib/rails/application/finisher.rb:79:in 'block in <module:Finisher>'
    from railties-8.1.3/lib/rails/initializable.rb:24:in 'BasicObject#instance_exec'
    from railties-8.1.3/lib/rails/initializable.rb:24:in 'Rails::Initializable::Initializer#run'
    from railties-8.1.3/lib/rails/application.rb:442:in 'Rails::Application#initialize!'
    from config/environment.rb:5:in '<main>'

System configuration

Rails version: 8.1.3

Ruby version: 3.4.4

Gem version: view_component 4.9.0 (zeitwerk 2.7.5, bootsnap 1.23.0)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions