Repository navigation
Conversation
Since Rails 8.0 routes are lazy-loaded in development and test, and are only drawn once a request reaches the router. Devise configures Warden when routes are drawn, but Warden::Manager runs earlier in the middleware stack and copies its configuration when building the proxy, so on the first request of a process Warden had no default scope, no strategies and no session serializers. This showed up as `:default user is not logged in`, the rememberable strategy not running after a restart, and `login_as`/`sign_in` storing the raw record in the session. Add a Devise::RoutesLoader middleware right before Warden::Manager that loads routes if they are not loaded yet. The first request was going to load them anyway, so nothing is lost for boot, console or tests. Also use lazy routes in every environment on Rails 8+, not only in development and test. Elsewhere Rails draws routes during initialization and `devise_for` has to load the model class to know which routes to draw, which Rails 8.2+ reports as a model loaded before the application was initialized whenever eager loading is off (e.g. rake tasks in production). With eager loading on, Rails loads routes at boot either way. Fixes heartcombo#5830, heartcombo#5844, heartcombo#5752, heartcombo#5847 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2E5SnkxkicWdSSbqUN4y
4 tasks done
Contributor
Author
|
Follow-up on the Rails side: rails/rails#58951 moves the That PR only fixes the default being chosen too late. The 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5830, #5844, #5752 and #5847.
Problem
Since Rails 8.0 routes are lazy-loaded in development and test (
Rails::Engine::LazyRouteSet): they are only drawn once a request reaches the router. Devise configures Warden while routes are drawn (Devise::RouteSet#finalize!), butWarden::Managerruns earlier in the middleware stack andWarden::Proxy#initializedupsmanager.configwhen it builds the proxy for the request. So the first request of a fresh process sees a Warden with no default scope, no strategies and no session serializers. That is what the reports describe::default user is not logged in(#5830), the rememberable strategy not running after a restart (#5844), andlogin_as/sign_instoring the raw record in the session and overflowing the cookie (#5752).Separately, Rails only uses lazy routes when
Rails.env.local?. Elsewhere routes are drawn during initialization, anddevise_forhas to load the model class to know which routes to draw. Rails 8.2's load-hook guard (rails/rails#56201) reports that as a component loaded before initialization whenever eager loading is off, e.g. rake tasks in production whererake_eager_loadis false (#5847).Fix
Devise::RoutesLoader, a two-line middleware inserted right beforeWarden::Manager, callsRails.application.reload_routes_unless_loaded. That is the same callDevise.mappingshas made since Make Devise.mappings work with lazy loaded routes. #5728 for code paths that are not requests (console, tests). The first request was going to draw the routes anyway, so nothing changes for boot time.devise.lazy_routesinitializer setsconfig.route_set_class = Rails::Engine::LazyRouteSetin every environment on Rails 8+, unless the app configured its own class. It runs beforemake_routes_lazy, which on current Rails main also instantiates the route set. When eager loading is on, Rails loads routes at boot either way (set_routes_reloader_hook), so production servers behave exactly as before; only non-eager boots like rake tasks stop drawing routes and loading models they never use.Both are guarded on
defined?(Rails::Engine::LazyRouteSet), so Rails 7.x is untouched.Why not #5831 or #5853
#5831 turns lazy routes off, which is the opposite of where Rails is going and keeps loading every Devise model at boot. #5853 registers models up front, but is opt-in, regresses
Devise.mappings(it no longer loads routes) and still trips the 8.2 guard in development. This keeps lazy routes working without the app having to know about it.Tests
test/lazy_routes_test.rbchecks the middleware order and boots the test app in a fresh process (test/scripts/first_request_probe.rb, since the suite has already loaded the routes) to assert that routes and theUsermodel are not loaded at boot, that the first request configures Warden fully, and that the same holds whenRails.env.local?is false. The lazy assertions are gated onLazyRouteSetso the test runs on every supported Rails.Also verified on a real Jumpstart-based app against Rails 8.2.0.alpha (rails/rails@4658033), fresh process each time:
POST /users/sign_indefault_scope=:default, no strategies, no failure appdefault_scope=:user,[:rememberable, :database_authenticatable], session key[[id], salt]rake_eager_load=false)Userloaded during bootLazyRouteSet, neither loadedNotes for Rails
While tracking this down, three things on the Rails side seemed worth mentioning:
make_routes_lazybeing gated onRails.env.local?. If Rails usedLazyRouteSeteverywhere, thedevise.lazy_routesinitializer here could go away.RoutesReloader#execute_unless_loadedis not synchronized on 8.0/8.1 (it is on main). Two concurrent first requests could draw the routes twice. This middleware does not make that worse thanDevise.mappingsalready does, but it is the reason the PR keeps the broadLazyRouteSetgate rather than version checks.ActionMailer::Basein an initializer that runs beforeaction_mailer.set_configs(mailpace-rails does) makes that railtie'son_load(:action_mailer)block callapp.routesbeforemake_routes_lazy, so the app silently gets a plainRouteSet.config.exceptions_app = routesinconfig/application.rbhas the same effect. Not Devise's to fix, but useful to know when an app does not reproduce these bugs.CI
The full 60-job matrix is green on the fork with this change combined with #5866 and #5867: https://github.com/excid3/devise/actions/runs/37403424517. On its own, the Rails 7.0/7.1/8.0 jobs need #5866 (json 3.0) and the rails-main job needs #5867 (8.2 test deprecations); none of those failures are related to this change. #5856 is the companion fix for the remaining 8.2 deprecation in
lib.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fb2E5SnkxkicWdSSbqUN4y