From 8054ad55c3d1b0602d3654cf0dfd065491f271b7 Mon Sep 17 00:00:00 2001 From: Taketo Takashima Date: Wed, 31 Dec 2025 22:45:09 +0900 Subject: [PATCH] Use `:unprocessable_content` in generated Devise config for Rack 3.1+, avoid Rack warnings (#5797) In Rack v3.1.0, the symbol for HTTP status code 422 was changed from `:unprocessable_entity` to `:unprocessable_content`. As a result, when using rack 3.2 with the following configuration in `config/initializers/devise.rb`, a warning is shown on login failure: ```ruby # config/initializers/devise.rb Devise.setup do |config| ... config.responder.error_status = :unprocessable_entity ``` Warning message: ```sh /path-to-app/vendor/bundle/ruby/3.4.0/gems/devise-4.9.4/lib/devise/failure_app.rb:80: warning: Status code :unprocessable_entity is deprecated and will be removed in a future version of Rack. Please use :unprocessable_content instead. ``` This warning can be resolved by updating the config as follows: ```diff # config/initializers/devise.rb Devise.setup do |config| ... + config.responder.error_status = :unprocessable_content - config.responder.error_status = :unprocessable_entity ``` This fixes the root cause of the warning for new apps by adjusting the generated config during `$ rails generate devise:install` depending on the rack version, so new apps using newer Rack versions generate `error_status = :unprocessable_content` instead of `:unprocessable_entity`. Existing apps are handled by [latest versions of Rails, which will now transparently convert the code under the hood to avoid the Rack warning](https://github.com/rails/rails/pull/53383), and Devise will use that translation layer when available in the failure app to prevent the warning there as well (since that isn't covered by Rails automatic conversion). Signed-off-by: Carlos Antonio da Silva --- CHANGELOG.md | 3 +++ README.md | 3 ++- app/controllers/devise/confirmations_controller.rb | 2 +- app/controllers/devise/unlocks_controller.rb | 2 +- lib/devise/failure_app.rb | 6 +++--- lib/generators/templates/devise.rb | 2 +- test/generators/devise_generator_test.rb | 1 - test/generators/install_generator_test.rb | 8 ++++++++ 8 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12abf235..47bbcf17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ * enhancements * Add Rails 8 support. - Routes are lazy-loaded by default in test and development environments now so Devise loads them before `Devise.mappings` call. [#5728](https://github.com/heartcombo/devise/pull/5728) + * New apps using Rack 3.1+ will be generated using `config.responder.error_status = :unprocessable_content`, since [`:unprocessable_entity` has been deprecated by Rack](https://github.com/rack/rack/pull/2137). + + Latest versions of [Rails transparently convert `:unprocessable_entity` -> `:unprocessable_content`](https://github.com/rails/rails/pull/53383), and Devise will use that in the failure app to avoid Rack deprecation warnings for apps that are configured with `:unprocessable_entity`. They can also simply change their `error_status` to `:unprocessable_content` in latest Rack versions to avoid the warning. * Add Ruby 3.4 and 4.0 support. * Reenable Mongoid test suite across all Rails 7+ versions, to ensure we continue supporting it. Changes to dirty tracking to support Mongoid 8.0+. [#5568](https://github.com/heartcombo/devise/pull/5568) * Password length validator is changed from diff --git a/README.md b/README.md index bb2dc697..e2025965 100644 --- a/README.md +++ b/README.md @@ -493,7 +493,8 @@ Devise.setup do |config| # apps is `200 OK` and `302 Found` respectively, but new apps are generated with # these new defaults that match Hotwire/Turbo behavior. # Note: These might become the new default in future versions of Devise. - config.responder.error_status = :unprocessable_entity + config.responder.error_status = :unprocessable_content # for Rack 3.1 or higher + # config.responder.error_status = :unprocessable_entity # for Rack 3.0 or lower config.responder.redirect_status = :see_other end ``` diff --git a/app/controllers/devise/confirmations_controller.rb b/app/controllers/devise/confirmations_controller.rb index 5e22079e..39ff669b 100644 --- a/app/controllers/devise/confirmations_controller.rb +++ b/app/controllers/devise/confirmations_controller.rb @@ -27,7 +27,7 @@ class Devise::ConfirmationsController < DeviseController set_flash_message!(:notice, :confirmed) respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } else - # TODO: use `error_status` when the default changes to `:unprocessable_entity`. + # TODO: use `error_status` when the default changes to `:unprocessable_entity` / `:unprocessable_content`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/app/controllers/devise/unlocks_controller.rb b/app/controllers/devise/unlocks_controller.rb index b1487760..8cff126c 100644 --- a/app/controllers/devise/unlocks_controller.rb +++ b/app/controllers/devise/unlocks_controller.rb @@ -29,7 +29,7 @@ class Devise::UnlocksController < DeviseController set_flash_message! :notice, :unlocked respond_with_navigational(resource){ redirect_to after_unlock_path_for(resource) } else - # TODO: use `error_status` when the default changes to `:unprocessable_entity`. + # TODO: use `error_status` when the default changes to `:unprocessable_entity` / `:unprocessable_content`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/lib/devise/failure_app.rb b/lib/devise/failure_app.rb index e1e24be4..d0b50f7d 100644 --- a/lib/devise/failure_app.rb +++ b/lib/devise/failure_app.rb @@ -77,9 +77,9 @@ module Devise flash.now[:alert] = i18n_message(:invalid) if is_flashing_format? self.response = recall_app(warden_options[:recall]).call(request.env).tap { |response| - response[0] = Rack::Utils.status_code( - response[0].in?(300..399) ? Devise.responder.redirect_status : Devise.responder.error_status - ) + status = response[0].in?(300..399) ? Devise.responder.redirect_status : Devise.responder.error_status + # Avoid warnings translating status to code using Rails if available (e.g. `unprocessable_entity` => `unprocessable_content`) + response[0] = ActionDispatch::Response.try(:rack_status_code, status) || Rack::Utils.status_code(status) } end diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index 9fe0ade8..b36f281f 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -305,7 +305,7 @@ Devise.setup do |config| # apps is `200 OK` and `302 Found` respectively, but new apps are generated with # these new defaults that match Hotwire/Turbo behavior. # Note: These might become the new default in future versions of Devise. - config.responder.error_status = :unprocessable_entity + config.responder.error_status = <%= Rack::Utils::SYMBOL_TO_STATUS_CODE.key(422).inspect %> config.responder.redirect_status = :see_other # ==> Configuration for :registerable diff --git a/test/generators/devise_generator_test.rb b/test/generators/devise_generator_test.rb index 00118c22..22112c69 100644 --- a/test/generators/devise_generator_test.rb +++ b/test/generators/devise_generator_test.rb @@ -37,5 +37,4 @@ class DeviseGeneratorTest < Rails::Generators::TestCase FileUtils.mkdir_p(destination) FileUtils.cp routes, destination end - end diff --git a/test/generators/install_generator_test.rb b/test/generators/install_generator_test.rb index 45aeddd0..3bb1b00f 100644 --- a/test/generators/install_generator_test.rb +++ b/test/generators/install_generator_test.rb @@ -23,4 +23,12 @@ class InstallGeneratorTest < Rails::Generators::TestCase assert_no_file "config/initializers/devise.rb" assert_no_file "config/locales/devise.en.yml" end + + test "responder error_status based on rack version" do + run_generator(["--orm=active_record"]) + + error_status = Rack::RELEASE >= "3.1" ? :unprocessable_content : :unprocessable_entity + + assert_file "config/initializers/devise.rb", /config\.responder\.error_status = #{error_status.inspect}/ + end end