rails: ActiveStorage::FileNotFoundError due to upload after commit

We were getting ActiveStorage::FileNotFoundErrors spuriously and are fairly confident that this is a race condition in the way ActiveStorage handles uploads and attachment.

Steps to reproduce

In the rails guides, the recommended set up for a controller that attaches a file to a model is as follows:

class SignupController < ApplicationController
  def create
    user = User.create!(user_params)
    redirect_to root_path
  end

  private
  def user_params
    params.require(:user).permit(:email_address, :password, :avatar)
  end
end

When using this recommended set up Rails will upload the file after committing the creation and attachment of the Blob to the model. Within the same thread this is not a problem because the hook will obviously wait until the upload is finished and other code runs that might depend on that file. However since the Blob and Attachment are already committed in the database, parallel processes like integrations polling for updates, or even just users clicking on the entity at just the wrong moment, will already see the attachment and try to download it causing an ActiveStorage::FileNotFoundError.

Expected behavior

Before the file is actually uploaded, the User should not be committed to the database. For updates, the Attachment should not be committed to the database before upload is finished.

Actual behavior

The new user is committed with the Blob attached before the file is uploaded. Other requests or background tasks can find that User causing an ActiveStorage::FileNotFoundError when they try to download the avatar.

Our workaround

Rewriting the controller like this solves the problem as the file is uploaded before the change is committed to the database: Edit: The original version of the workaround that I posted broke presence-validation. With this updated variant, we still just assign nil to the avatar, if the param is not sent, so displaying form errors still works normally.

class SignupController < ApplicationController
  def create
    user = User.new(user_params.except(:avatar)) # Note this change, to avoid creating the attachment twice.
    user.avatar = try_create_and_upload_blob!(user_params.fetch(:avatar, nil))

    user.save!
    redirect_to root_path
  end

  private
  def user_params
    params.require(:user).permit(:email_address, :password, :avatar)
  end

  # This can also be put somewhere as a static method for reuse in different controllers.
  def try_create_and_upload_blob!(uploaded_file)
    return nil if uploaded_file.blank?
    ActiveStorage::Blob.create_and_upload!(
      io: uploaded_file.to_io,
      filename: uploaded_file.original_filename,
    )
  end
end

This makes sure, that the file is completely uploaded before the Blob is attached to the User. Of course this is quite a bit more cumbersome.

Other considerations

I could also see other problems arising from doing a long upload within a transaction, so I was wondering if this was intentional.

If upload it is really supposed to be happening in the after_commit, at least the guide should point out these potential issues and maybe we can even find a way to make uploading before committing less verbose and cumbersome.

Potentially related issues

These issues might be related:

System configuration

Rails version: 6.1.6

Ruby version: 3.1.3

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 7
  • Comments: 16 (5 by maintainers)

Most upvoted comments

We’re seeing the same issue in an ActiveJob/Sidekiq job which results in the file being lost due to the job exiting before the after_commit fires.

Adding a flag on Blob would be useful and allow us to ensure the job doesn’t exit early.

This seems like the expected behavior. Maybe we should have a flag in the Blob record to say the image isn’t yet uploaded to the service so users can check that flag, but I don’t see how to change this behavior so the record is only committed when it is uploaded to the service without decreasing considerably the resilience of applications.