rails: Rails 7.1: incorrect path when submitting form for a singular resource

Steps to reproduce

When submitting a form with an error, Rails 7.1 redirects to incorrect path when the resource is singular. This may be an issue with Turbo (see expected and actual behaviors below).

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "rails", "~> 7.1.0"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :authors, force: true do |t|
    t.string :name
  end
end

class Author < ActiveRecord::Base
  validates :name,
            format: { with: /\A[a-zA-Z0-9-]{0,39}\z/, message: 'can only contain alphanumeric characters and dashes' },
            length: { maximum: 39 }
end

require "rack/test"
require "action_controller/railtie"

class TestApp < Rails::Application
  config.root = __dir__
  config.hosts << "example.org"
  config.session_store :cookie_store, key: "cookie_store_key"
  config.secret_key_base = "secret_key_base"

  config.logger = Logger.new($stdout)
  Rails.logger  = config.logger

  routes.draw do
    resource :author, only: %i[edit update]
  end
end

class AuthorsController < ActionController::Base
  include Rails.application.routes.url_helpers

  before_action :set_author, only: %i[edit update]

  def edit; end

  def update
    if @author.update(author_params)
      redirect_to edit_author_path
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def set_author
    @author = Author.find(1) # Actual application would use session to find author logged in
  end

  def author_params
    params.require(:author).permit(:name)
  end
end

class BugTest < Minitest::Test
  include Rack::Test::Methods

  Author.create!(name: 'name')

  def test_author_updated_success
    patch '/author', { author: { name: 'validName' } }
    assert_equal 'http://example.org/author/edit', last_response.headers['location']
  end

  def test_author_updated_failure
    patch '/author', { author: { name: 'invalid!!!name?' } }
    assert_equal 'http://example.org/author/edit', last_response.headers['location']
  end

  private

  def app
    Rails.application
  end
end

Expected behavior

The request path should be author/edit. The console in Rails 7.0.8 showed

Started PATCH "/author" for 127.0.0.1 at 2023-10-12 09:22:13 -0400
Processing by AuthorsController#update as TURBO_STREAM

Actual behavior

The request path is author/#{author.id}”. The console in Rails 7.1.0 shows

Started PATCH "/author.1" for 127.0.0.1 at 2023-10-12 09:21:10 -0400
Processing by AuthorsController#update as */*

System configuration

Rails version: 7.1.0

Ruby version: 3.2.2

About this issue

  • Original URL
  • State: open
  • Created 9 months ago
  • Comments: 30 (20 by maintainers)

Most upvoted comments

Thank you @eizengan.

I’ve decided to open this back up for the time being given that the nested resources will not work with resolve.

As I noted above we do have it documented for how to use resource non-nested. However using nested resources is a popular use case and I think adding support for this will be beneficial to the framework.

I tested this locally.

In routes.rb both

  resources :posts do
    resource :author, resolve: { Author: [:author, :post] }
  end

and

  resources :posts do
    resource :author
  end
  resolve("Author") { [:post, :author] }

Result in:

Started PATCH "/posts/1/author.1"

My PR #49889 addresses and fixes both use cases, nested or not.

Thanks for your input. My PR doesn’t use resources. I tested your use case and it works fine in my suggested fix linked in the above PR.

Understood, but there appear to be two competing philosophies here:

  • treat as a regression in polymorphic_path
  • treat as a side effect of failing to use resolve when necessary/appropriate, now exposed by enhancements to underlying infrastructure

My comment regarding there being no working path for nested singular resources is aimed at fixes adhering to the latter, not the former

That’s why I asked if it would be correct to change the current convention and that resource … internally sets the resolve

I don’t have the context to say whether that’s the best approach, but to restore parity with pre-7.1 behavior we would also need to make resolve work with nested singular resources. Given that this is specifically called out in the docs (the NOTEs on my earlier comment) I suspect that could be a nontrivial enhancement

So I was informed by Rafael that my implementation was not a viable solution. So anyone can feel free to open other PRs to fix this! I’ll try again as well, time permitting.

Thanks for your input. My PR doesn’t use resources. I tested your use case and it works fine in my suggested fix linked in the above PR.

Started PATCH "/posts/1/author" for 127.0.0.1 at 2023-10-26 15:29:05 -0400
Processing by AuthorsController#update as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "author"=>{"name"=>"validName"}, "commit"=>"Update Author", "post_id"=>"1"}
  Author Load (1.6ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/authors_controller.rb:67:in `set_author'
Redirected to http://127.0.0.1:3000/posts/1/author/edit
Completed 302 Found in 15ms (ActiveRecord: 1.6ms | Allocations: 1049)


Started GET "/posts/1/author/edit" for 127.0.0.1 at 2023-10-26 15:29:05 -0400
Processing by AuthorsController#edit as TURBO_STREAM
  Parameters: {"post_id"=>"1"}
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/authors_controller.rb:67:in `set_author'
  Rendering layout layouts/application.html.erb
  Rendering authors/edit.html.erb within layouts/application
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/authors/edit.html.erb:3
  Rendered authors/_form.html.erb (Duration: 1.3ms | Allocations: 355)
  Rendered authors/edit.html.erb within layouts/application (Duration: 2.8ms | Allocations: 916)
  Rendered layout layouts/application.html.erb (Duration: 3.5ms | Allocations: 1303)
Completed 200 OK in 6ms (Views: 3.9ms | ActiveRecord: 0.6ms | Allocations: 2278)

Thank you @jhawthorn! Using resolve works on my end.

As @matthewd said. This is clearly a bug. The /author.1 is the wrong path for a single resource configuration. I will try to research polymorphic_url today.

Yes, I thought I clarified that in my comment above after he chimed in?

I was going to upgrade my application to 7.1, but I have some single resources configured. So I want to fix it.

Great! Thanks for taking the time to look into this.