This is a minimal reproduction of a Devise/Warden issue on Rails 8: when
config.eager_load is false, the first request to a newly booted server
tries to authenticate before the strategies have been loaded and 401s. After
that, all other requests authenticate normally.
Ruby 4.0.2 - Rails 8.1.3.1 - Devise 5.0
A stock Rails app with a Devise User and one custom Warden strategy,
Strategies::ProxyHeaderAuthenticatable (lib/strategies/) that simulates a
proxy header authentication. It is session-less, so every request reads the
header again.
config/initializers/devise.rb:
Rails.application.config.to_prepare do
Warden::Strategies.add(:proxy_header, Strategies::ProxyHeaderAuthenticatable)
end
# ...inside Devise.setup:
config.warden do |warden_config|
warden_config.default_strategies(scope: :user).unshift :proxy_header
endGET / is HomeController#index, behind authenticate_user!.
bin/setupThe failure happens once per boot, so each attempt needs a new process.
bin/rails serverThen, at least three times:
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" \
-H "X-Proxy-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Proxy-User-Email: devise@thoughtbot.test" \
http://localhost:3000/request 1: 302 -> http://localhost:3000/users/sign_in <- the bug
request 2: 200
request 3: 200
...
Restart the server to see it again.
For simplicity, this app is not built to be tested in a browser.
From 8, Rails defers drawing routes until the first request unless eager loading is on. Devise only applies its Warden configuration when routes finalize loading. This is why the first cold request fails to authenticate.
In my project, this is the configuration I added to workaround this issue:
Rails.application.config.after_initialize do |app|
app.reload_routes! unless app.config.eager_load
end| Branch | request 1 | 2 | 3 |
|---|---|---|---|
main (devise 5.0.4) |
302 | 200 | 200 |
testing-devise-pr-5831 |
200 | 200 | 200 |
testing-devise-pr-5853 |
200 | 200 | 200 |
Disable Rails lazy routes with Devise - heartcombo/devise@ca-routes-eager
This fix adds a devise.make_routes_eager_load initializer that sets
config.route_set_class back to ActionDispatch::Routing::RouteSet, so routes
finalize during boot. Fixes the bug with no application changes, but opts the
app out of Rails 8 lazy routes whenever Devise is loaded.
Allow early configuration of devise models - kaisq/devise@devise-model-registration
This fix keeps lazy routes. It splits a mapping into a model phase and a routing
phase, moves Devise.configure_warden! into config.after_initialize, and asks
the app to opt in per scope:
Devise.setup do |config|
config.devise_model :users
endSwapping the gem without that line causes a regression that makes all requests
fail. configure_warden! is memoized, so it runs once at after_initialize
with no mappings registered, and the call in finalize! is removed - nothing
reconfigures Warden once routes load.
| request 1 | 2 | 3 | |
|---|---|---|---|
| gem swap only | 302 | 302 | 302 |
gem swap + config.devise_model |
200 | 200 | 200 |