Rails 6.1 -> 7 gives 404 for ActionText attachments


I’ve recently upgraded a project from Rails 6.1 → 7 and noticed that in production I’m receiving 404s on ActionText attachments.

I can’t see anything in the upgrade guide that needs to be actioned around ActionText specifically.

When I look at the RichText record where the images fail, I can see that body.attachments has a number of RemoteImage attachables that were uploaded to the 6.1 app. These are producing 404s

When I upload a new image to the record under 7.x, it is being attached to the record as a Blob and displays correctly.

Anyone have any pointers for me on this?

1 Like

ActionText relies on ActiveStorage, which generates encrypted urls. When you submit, ActionText saves that URL as part of the HTML code, so if something causes ActiveStorage urls to change, the urls in ActionText will no longer work.

The migration from rails 6 to 7 changed the key generator from SHA1 to SHA256, which should have impacted the urls in ActiveStorage, which in turn must have broken your images. Here’s a sample code I used to fix mine:

  # After active storage urls are changed, use this to recreate all trix attachments
  def self.refresh_trixes
    ActionText::RichText.where.not(body: nil).find_each do |trix|

  # After active storage urls are changed, use this to recreate a specific trix attachments
  def self.refresh_trix(trix)
    return unless trix.embeds.size.positive?

    trix.body.fragment.find_all("action-text-attachment").each do |node|
      embed = trix.embeds.find { |attachment| attachment.filename.to_s == node["filename"] && attachment.byte_size.to_s == node["filesize"] }

      node.attributes["url"].value = Rails.application.routes.url_helpers.rails_storage_redirect_url(embed.blob, host: "YOUR_DOMAIN")
      node.attributes["sgid"].value = embed.attachable_sgid

    trix.update_column :body, trix.body.to_s

Pleas make sure you test this before running in production. I’ve used a few times and it works well enough, but you never know… and also replace YOUR_DOMAIN with your actual domain.


You are a legend, thank you.

Is there something I missed or is this some undocumented behaviour?

It’s undocumented. I had to read through the code to figure out why images were broken and how to fix them. Since I rely heavily in signed_id, I’ve created this initializer to block deploy in case something we did caused them to change (which would break action text and a lot of other things)

Rails.application.config.after_initialize do
  if Rails.env.production? && User.find_signed("A_SAMPLE_SIGNED_ID").nil?
    raise ArgumentError, "Something has changed the signed ids. DO NOT DEPLOY"

So I grabbed my user, did user.signed_id in production, and placed the result in A_SAMPLE_SIGNED_ID. This way, next time something like the SHA causes the signed ids to change the initializer will raise and block my deploy.


That’s quite an oof …

Are we the only two people that use ActionText?

1 Like

Probably not many apps using action text, and even less who made the Rails 6 → 7 migration already. But I know of another dev, who maintains a blogging sass, who had the same problem and came with a similar solution to mine after also reading through the source code

I’m the third one, lol :slight_smile: Thanks for the fix guys.

Thanks for providing this! I’m using it for a different reason: Moving the assets to a different AWS S3 bucket. I’m trying to create different buckets for the different environments so that I can do testing. I’m getting the following error:

NoMethodError: undefined method rails_storage_redirect_url’`

I am on Rails I’m not seeing any documentation on this particular method (rails_storage_redirect_url). Is there something I’m missing?

Thanks for any help you can provide!

Yes, that method was only added in Rails 6.1 with the proxy feature. In your case I think you need to use rails_blob_path. Check the 6.0 guide, section “Linking files”

Awesome!! Thanks for the quick response.

Sorry if I should create a new topic for this question. I can if needed! But maybe you can help steer me in the right direction. We originally had one single Amazon S3 bucket to store our active-storage assets. I’m trying to create a development environment so that we can do dev/testing and not mess up production assets.

Important detail: We create courses on our app/site that have text, images, gifs, etc. The fields use ActionText to create the content. We usually create the content in production. Occasionally, we’ll snapshot the production database, and use the copy as our updated development database. I am unsure how this plays out with how active storage works.

So, let’s say I have this setup now:


But now I want:



Let’s say my domains are:

Production: example-domain.org Staging: staging.example-domain.org Development: localhost:5000

What do you recommend would be the best way to create a development environment for this actiontext/active-storage setup but not mess up our production environment?

Here’s what we do:

  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: sa-east-1
  bucket: <%= Rails.application.credentials.dig(:festalab, :cloud_store_rails) %>
  public: true
  http_open_timeout: 5
  http_read_timeout: 5
  retry_limit: 2

I don’t remember if Rails 6.0 had environment specific credentials, but you can just use ENV["RAILS_ACTIVE_STORAGE_BUCKET"] of a similar env variable to have separate buckets for production and development.

Then you set the production bucket to mirror every file to the development bucket (which of course means you are doubling your storage costs).

ActionText will still have it’s saved urls pointed to the production url, but when if something causes it to purge a file, it will do so on the development bucket, not the production one.

Of course, I’m not sure if that will work on Rails 6.0 since I only started doing this on 6.1, so you will have to test, but that’s the basic idea.

Interesting! Does this mean that I don’t actually need to do this migration to change the URL?

Also, I forgot to ask… How do you set the production bucket to mirror to the development bucket like you described?

That’s a bit complicated and there are several steps to get it working, specially if you need to replicate existng files. Here are two articles:

And for reference, here’s mine:

Ah. I wasn’t sure if you were referring to Rails’s “Mirror Service” feature:

I am currently upgrading my rails to 6.1 because I thought that’s what you were using, and it seems it’s not compatible with 6.0.

It seems like it would do a similar behavior?

Yes, but that means your servers are taking on extra load they don’t need to and taking longer to respond to other requests. Better let AWS do it themselves.

@brenogazzola I’m back to this thread again! This time, I’m actually in the process of updating from rails 6.1 to 7.

I am running into the issue where once I deploy the Rails 7 changes to production, the images (that we uploaded into the content elements via actiontext) are broken:

I took your code from above, modified it a little, and made a rake task. Here is the code:

namespace :refresh_trix do
  desc "To refresh trix attachments due to Rails 6 to 7 upgrade"
  puts Rails.env
  task refresh_embeds: :environment do
    ActionText::RichText.where.not(body: nil).find_each do |trix|
      next unless trix.embeds.size.positive?

      trix.body.fragment.find_all("action-text-attachment").each do |node|
        # puts "node is: "
        # puts node.inspect
        embed = trix.embeds.find { |attachment| attachment.filename.to_s == node["filename"] && attachment.byte_size.to_s == node["filesize"] }

        puts "Found one. Original url is: "
        puts node.attributes["url"].value

        puts "Original sgid is: "
        puts node.attributes["sgid"].value
        if embed.present?
          #puts "embed.blob is: "
          #puts embed.blob.inspect

          # Files
          node.attributes["url"].value = Rails.application.routes.url_helpers.rails_storage_redirect_url(embed.blob, host: "https://www.cs2n.org")
          node.attributes["sgid"].value = embed.attachable_sgid

          puts "New url is: "
          puts node.attributes["url"].value
          puts "New sgid is: "
          puts node.attributes["sgid"].value

      puts "Body is now: "
      puts trix.body.to_s


      #trix.update_column :body, trix.body.to_s

I’m having it “break” after one iteration so that I could see what’s going on. Here is the logger output I have:

Found one. Original url is:
Original sgid is:
New url is:
New sgid is:
Body is now:
  ActiveStorage::Blob Load (30.6ms)  SELECT `active_storage_blobs`.* FROM `active_storage_blobs` WHERE `active_storage_blobs`.`id` = 2148 LIMIT 1
  Rendered active_storage/blobs/_blob.html.haml (Duration: 35.4ms | Allocations: 10706)
  Rendered /Users/vnguyen/.rvm/gems/ruby-2.7.1/gems/actiontext-7.0.2/app/views/action_text/contents/_content.html.erb within layouts/action_text/contents/_content (Duration: 84.5ms | Allocations: 13274)
<div class="trix-content">
  <div>Power on your VEX Brain. On your desktop, find and launch the VEXcode V5 Pro software.<br><br><action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSIzZ2lkOi8vY3Mybi9BY3RpdmVTdG9yYWdlOjpCbG9iLzIxNDg_ZXhwaXJlc19pbgY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw--30154bfac9fd69e0d142b7d701b56a6a917ee0a2" content-type="image/png" url="https://www.cs2n.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbVFJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--5c0bcf4fcb0617930b18e514f0341890ca0650ae/image.png" filename="image.png" filesize="4356" width="90" height="90" previewable="true" presentation="gallery" caption="VEXcode V5 Pro"><figure class="attachment attachment--preview attachment--png">
<img src="http://example.org/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbVFJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--5c0bcf4fcb0617930b18e514f0341890ca0650ae/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJHa0NBQU09IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--9fa28db11c216db60bd6118944e58f8742ab52d8/image.png">
<figcaption class="attachment__caption">VEXcode V5 Pro</figcaption>

After deploying the Rails 7 upgrade to production, I tried to run this (uncommenting out the one line where it actually updates the column), but it doesn’t seem to fix the one image it found.

Any pointers you can provide would be great. Thanks so much!

Weird. Did you check if the update worked? By that I mean checking that the url in the body actually changed after the update?

I remember I had some trouble with it and had to change the command between versions because it had stopped working. The line you have is the one I use currently (I follow Rails Edge, not release versions), so version 7.0 might need something a bit different?

@brenogazzola So, two things:

  1. Not sure if it matters, but I have it set to 7.0.2
  2. Because the content in the actiontext-created data was created in the production platform, when I go and update the local/development and staging environments with Rails 7.0.2, I don’t see any changes. (Some background info: we snapshot the production database in order to use for development/staging so that we have updated/real data). Should I be seeing something different in the development/staging database?

EDIT: Oh! I think I understand what you meant by “checking that the URL in the body actually changed after the update”. You meant if it change the database record after that line. Good point. I’ll check that.

@brenogazzola Ok. I finally got time to record one image before and after the script is ran. The record that changes is in the “body” field of the “action_text_rich_texts” table.


'423', 'content', '<div>Power on your VEX Brain. On your desktop, find and launch the VEXcode V5 Pro software.<br><br><action-text-attachment sgid=\"BAh7CEkiCGdpZAY6BkVUSSIzZ2lkOi8vY3Mybi9BY3RpdmVTdG9yYWdlOjpCbG9iLzIxNDg_ZXhwaXJlc19pbgY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw--e16e52d244fcb768d902977f199e77b853675c69\" content-type=\"image/png\" url=\"https://www.cs2n.org/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbVFJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2440b027ff68860db4b94e34c580f46c4095bd2d/image.png\" filename=\"image.png\" filesize=\"4356\" width=\"90\" height=\"90\" presentation=\"gallery\" caption=\"VEXcode V5 Pro\"></action-text-attachment></div>', 'ContentElement', '426', '2020-08-06 03:59:58', '2021-04-22 15:17:21'


'423', 'content', '<div class=\"trix-content\">\n  <div>Power on your VEX Brain. On your desktop, find and launch the VEXcode V5 Pro software.<br><br><action-text-attachment sgid=\"BAh7CEkiCGdpZAY6BkVUSSIzZ2lkOi8vY3Mybi9BY3RpdmVTdG9yYWdlOjpCbG9iLzIxNDg_ZXhwaXJlc19pbgY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw--30154bfac9fd69e0d142b7d701b56a6a917ee0a2\" content-type=\"image/png\" url=\"https://www.cs2n.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbVFJIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--5c0bcf4fcb0617930b18e514f0341890ca0650ae/image.png\" filename=\"image.png\" filesize=\"4356\" width=\"90\" height=\"90\" previewable=\"true\" presentation=\"gallery\" caption=\"VEXcode V5 Pro\"></action-text-attachment>\n</div>\n</div>', 'ContentElement', '426', '2020-08-06 03:59:58', '2021-04-22 15:17:21'

I did a diff of the two, and it shows this:

So it seems to:

  1. Add a class to the div (trix-content)
  2. Modifies the sgid
  3. Modifies the url