Search code examples
ruby-on-railsgithub-actionsmiddleware

Rails Custom Middleware not being recognized in GH CI


I'm using custom middle ware for the first time to normalize error handling. I have my middleware defined in app/middleware/error_handler.rb and using in my main application.rb

...
require_relative "../app/middleware/error_handler"

module AppName
  class Application < Rails::Application
    ...
    config.middleware.use Middleware::ErrorHandler
    ...
  end
end

This all works fine in tests and in development when routing to localhost. But when I push to github and the action takes over I get this after it performs the migrations

An error occurred while loading spec_helper.
Failure/Error: require_relative "../config/environment"

NameError:
  uninitialized constant ErrorHandler
# ./config/environment.rb:5:in `<top (required)>'
# ./spec/rails_helper.rb:6:in `require_relative'
# ./spec/rails_helper.rb:6:in `<top (required)>'
# ./spec/spec_helper.rb:3:in `<top (required)>'
No examples found.

Here is my action/workflow

name: Run RSpec

on:
  push:
    branches:
      - "*" # Trigger on push to any branch

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16.1
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: ${{ secrets.ROUTE_RATER_DATABASE_PASSWORD }}
          POSTGRES_DB: postgres
        ports:
          - 5432:5432
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

      redis:
        image: redis
        ports:
          - 6379:6379
        options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3.0"

      - name: Install dependencies
        run: |
          gem install bundler
          bundle install --jobs 4 --retry 3

      - name: Setup DB, Run tests
        env:
          PGHOST: localhost
          PGUSER: postgres
          PGPORT: ${{ job.services.postgres.ports[5432] }}
          PGPASSWORD: ${{ secrets.DATABASE_PASSWORD }}
          REDIS_URL: redis://localhost:6379/1
          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
          RAILS_ENV: test
        run: |
          bin/rails db:create db:migrate db:schema:load
          bundle exec rspec


Solution

  • app/middleware/error_handler.rb is expected to define ErrorHandler, but you're defining a namespaced Middleware::ErrorHandler.

    Normally, if you were to ask for ErrorHandler it would be loaded from the first error_handler.rb file found in any of the root directories, which is app/middleware/ in this case. File structure have to correspond to module/class names relative to a root directory:

    # app/middleware/error_handler.rb
    #                ^^^^^^^^^^^^^
    #                      |
    class ErrorHandler # >-'
    end
    

    Rails automatically configures directories directly under app/ to be root directories. With this you can just load ErrorHandler without requiring it:

    >> ErrorHandler
    => ErrorHandler
    

    If you were to namespace it:

    # app/middleware/error_handler.rb
    
    module Middleware
      class ErrorHandler
      end
    end
    

    You get an error:

    >> ErrorHandler
    (irb):1:in `<main>': uninitialized constant ErrorHandler (NameError)
    
    ErrorHandler
    ^^^^^^^^^^^^
    

    Which is exactly what zeitwerk is doing by eager loading your app in production or in ci test environment:

    # config/environments/test.rb
    
    config.eager_load = ENV["CI"].present?
    

    In development, you can double check that your app can be eager loaded:

    $ bin/rails zeitwerk:check
    
    Hold on, I am eager loading the application.
    expected file app/middleware/error_handler.rb to define constant ErrorHandler, but didn't
    

    Since you can't autoload reloadable code during boot anyway you have to require and use Middleware::ErrorHandler, just don't put it in autoload path.

    You can also use autoload_once_paths or autoload_lib_once, which autoloads non-reloadable code, but you have to move config.middleware.use into an initializer:

    # config/application.rb
    
    config.autoload_lib_once(ignore: %w(assets tasks))
    
    # config/initializers/middleware.rb
    
    Rails.application.config.middleware.use Middleware::ErrorHandler
    
    # lib/middleware/error_handler.rb
    
    module Middleware
      class ErrorHandler
        # ...
      end
    end