rails: ActiveStorage contaminating model instances resulting in ActiveRecord::AssociationTypeMismatch

Using ActiveStorage can result in the class instance becoming contaminated in a strange way that results in a ActiveRecord::AssociationTypeMismatch exception when trying to associate the instance with another ActiveRecord model.

This example may seem a little unconventional as I discovered this issue whilst building seeds for my application.

Steps to reproduce

require 'open-uri'

class User
  has_one_attached :avatar
  has_many :talks
end

class Talk
  belongs_to :user
end

user_a = User.create!({ email: 'user.a@example.com' })
user_b = User.create!({ email: 'user.b@example.com' })

user_a.avatar.attach(io: open('https://example.com/any/valid/image/uri'))

user_a.class.to_s
# => "User"

user_b.class.to_s
# => "User"

Talk.new({ user: user_a })
# => ActiveRecord::AssociationTypeMismatch

Talk.new({ user: user_b })
# => true

Expected behavior

Associating user_a to a new instance of Talk should return true.

Actual behavior

The following exception is unexpectedly raised:

ActiveRecord::AssociationTypeMismatch (User(#70172285884400) expected, got #<User id: "5c01db01-fd4d-464c-bb36-33d29469fe77", email: "example@example.com", created_at: "2019-10-29 21:20:16", updated_at: "2019-10-29 21:20:45"> which is an instance of User(#70172283718280))

Further information

I added a binding.pry into the method raise_on_type_mismatch! in activerecord-6.0.0/lib/active_record/associations/association.rb. I’ve included the output below:

From: /Users/adambutler/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/associations/association.rb @ line 275 ActiveRecord::Associations::Association#raise_on_type_mismatch!:

    273: def raise_on_type_mismatch!(record)
    274:   unless record.is_a?(reflection.klass)
 => 275:     binding.pry
    276:     fresh_class = reflection.class_name.safe_constantize
    277:     unless fresh_class && record.is_a?(fresh_class)
    278:       message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
    279:         "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
    280:       raise ActiveRecord::AssociationTypeMismatch, message
    281:     end
    282:   end
    283: end

[1] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record.is_a?(reflection.klass)
=> false

[2] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record
=> #<User id: "a19c59f1-fd39-40da-abd8-42bc94f2bfc7", email: "user@example.com", created_at: "2019-10-29 21:32:33", updated_at: "2019-10-29 21:32:34">

[3] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record.class
=> User(id: uuid, email: string)

[4] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> reflection.klass
=> User(id: uuid, email: string)

System configuration

Rails version: 6.0.0

Ruby version: 2.6.5

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 8
  • Comments: 15 (6 by maintainers)

Commits related to this issue

Most upvoted comments

That’s a serious bug 🙄 I’m surprise that issue doesn’t have many 👍

I just ran through the reproduction steps for this issue on the latest commit and I can no longer reproduce it, so I am going to close this issue.

I was not able to reproduce the issue exactly as stated, but I was able to reproduce and investigate something quite similar, involving the interaction between Active Storage, Spring, and the EventedFileUpdateChecker.

Reproduction steps

  • Run:
rails new test_app
cd test_app
bin/rails active_storage:install
bin/rails g model account
bin/rails db:migrate
  • Add has_one_attached :profile_photo to Account
  • Add config.active_job.queue_adapter = :inline to config/environments/development.rb
  • Run:
bin/rails runner '
  require "open-uri"
  account = Account.create!
  account.profile_photo.attach(io: open("https://thoughtbot.com/brand_assets/2:1648.svg"), filename:
"test")
  puts Account == account.class
'

(If it is easier I could put this together into a repo and share a link to that.)

Expected behavior

I expected this to print true

Actual behavior

Account is getting reloaded in the middle of the example, causing this to return false

What is going on?

This came up in https://github.com/thoughtbot/factory_bot/issues/1330 as an ActiveRecord::AssociationTypeMismatch error. I closed the issue when I could reproduce it without using factory_bot at all.

Workarounds

This bug involves the interaction between spring, active_job, and the evented file update checker. Removing any one of these makes this problem go away:

  • Disable spring - this puts the initialization of the EventedFileUpdateChecker in the same process as the updated? check.
  • Use the FileUpdateChecker instead, which doesn’t have this problem
  • Disable Active Storage to avoid its execute callbacks (this seems like the worst option!)

Versions

Rails 6.0.3.2 Ruby 2.6.6

I think I understand the problem fairly well at this point, but I am unsure what the best way to go about fixing it is. I would like to chat with somebody more familiar with both the file checker and Rails callbacks, if possible.

I would like to ask that this issue is reopened and the problem can be reproduced on Rails 6.0.2.2