How we upgraded from Rails 6 to Rails 7

Abhishek T

Abhishek T

September 20, 2022

Recently, we upgraded all neeto products to Rails 7 using Rails upgrade guide.

Here are the issues we faced during the upgrade.

Migrating to Active Record Encryption

This was the biggest challenge we faced during the upgrade. For encrypting columns, we had used the attr_encrypted gem. However Rails 7 came with Active Record Encryption. So we needed to decrypt the records in the production database and encrypt them using Active Record Encryption. We found that "attr_encrypted" gem was incompatible with Rails 7. So the only option was to remove the "attr_encrypted" gem and decrypt the records using a script. We used the following method to decrypt the records.

def decrypted_attribute(attribute_name, record)
  value = record.send(attribute_name)
  return if value.blank?

  value = Base64.decode64(value)

  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  cipher.decrypt

  cipher.key = Rails.application.secrets.attr_encrypted[:encryption_key]
  cipher.iv = Base64.decode64(record.send(:"#{attribute_name}_iv"))

  cipher.auth_tag = value[-16..]
  cipher.auth_data = ""

  cipher.update(value[0..-17]) + cipher.final
end

Broken images in Active Storage

After the upgrade, we started getting broken images in some places. This happened for Active Storage links embedded in Rich Text. After some debugging we found that we were getting incorrect Active Storage links because of a change in the key generation algorithm. The following configuration was loading images using the old algorithm.

config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA1

Since the new algorithm provides more security, we decided to migrate the links instead of using the old algorithm. We used the following code to migrate the old link to the new valid link.

# Usage:
# text_with_new_links = ActiveStorageKeyConverter.new(text_with_old_links).process
# If no links are there to replace, the original text will be returned as it is.

class ActiveStorageKeyConverter
  def initialize(text)
    @text = text
  end

  def process
    replace(@text)
  end

  private
    def convert_key(id)
      verifier_name = "ActiveStorage"
      key_generator =  ActiveSupport::KeyGenerator.new(Rails.application.secrets.
      secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1)
      key_generator = ActiveSupport::CachingKeyGenerator.new(key_generator)
      secret = key_generator.generate_key(verifier_name.to_s)
      verifier = ActiveSupport::MessageVerifier.new(secret)

      ActiveStorage::Blob.find_by_id(verifier.verify(id, purpose: :blob_id))
      .try(:signed_id) rescue nil
    end

    def replace(text)
      keys = text.scan(URI.regexp).flatten.select{|x|
      x.to_s.include? ("rails/active_storage")}.map{|x| x.split("/")[-2]}
      keys.each do |key|
        new_key = convert_key(key)
        text = text.gsub(key, new_key) if new_key
      end
      text
    end
end

Following one time rake task was used to update the Active Storage links in the content column of Task model:

desc "Update active storage links embedded in rich text to support in rails 7"
task migrate_old_activestorage_links: :environment do

  table_colum_map = {
    "Task" => "content",
  }
  match_term = "%rails/active_storage%"

  table_colum_map.each do |model_name, column_name|
    model_name.to_s.constantize.where("#{column_name} ILIKE ?", match_term).find_each do|row|
      row.update_column(column_name, ActiveStorageKeyConverter.new(row[column_name]).process)
    end
  end
end

Test failures with the mailer jobs

After upgrading to Rails 7, tests related to mailers started to fail. This was because the mailer jobs were enqueued in the default queue instead of mailers. We fixed this by adding the following configuration.

config.action_mailer.deliver_later_queue_name = :mailers

Autoloading during initialization failed

After the upgrade if we start Rails sever then we were getting the following error.

$ rails s
=> Booting Puma
=> Rails 7.0.3.1 application starting in development
=> Run `bin/rails server --help` for more startup options
Exiting
/Users/BB/Neeto/neeto_commons/lib/neeto_commons/initializers/session_store.rb:13:in
`session_store': uninitialized constant #<Class:NeetoCommons::Initializers>::ServerSideSession     (NameError)

    ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
                                                                ^^^^^^^^^^^^^^^^^
   from /Users/BB/Neeto/neeto-planner-web/config/initializers/common.rb:10:in `<main>'

That error was coming from our internal neeto-commons initializer called session_store.rb. The code looked like this.

#session_store.rb

module NeetoCommons
  module Initializers
    class << self
      def session_store
        Rails.application.config.session_store :active_record_store,
          key: Rails.application.secrets.session_cookie_name, expire_after: 10.years.to_i

        ActiveRecord::SessionStore::Session.table_name = "server_side_sessions"
        ActiveRecord::SessionStore::Session.primary_key = "session_id"
        ActiveRecord::SessionStore::Session.serializer = :json
        ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
      end
    end
  end
end

In order to fix the issue we had to put the last statement in a block like shown below.

Rails.application.config.after_initialize do
 ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
end

Missing template error with pdf render

After the Rails 7 upgrade the following test started failing.

def test_get_task_pdf_download_success
  get api_v1_project_section_tasks_download_path(@project.id, @section, @task, format: :pdf)

  assert_response :ok

  assert response.body.starts_with? "%PDF-1.4"
  assert response.body.ends_with? "%EOF\n"
end

The actual error is Missing template api/v1/projects/tasks/show.html.erb.

In order to fix it we renamed the file name from /tasks/show.html.erb to /tasks/show.pdf.erb. Similarly we changed the layout from /layouts/pdf.html.erb to /layouts/pdf.pdf.erb.

Initially the controller code looked like this.

format.pdf do
  render \
    template: "api/v1/projects/tasks/show.html.erb"
    pdf: pdf_file_name,
    layout: "pdf.html.erb"
end

After the change the code looked like this.

format.pdf do
  render \
    template: "api/v1/projects/tasks/show",
    pdf: pdf_file_name,
    layout: "pdf"
end

Open Redirect protection

After the Rails 7 upgrade the following test started failing.

def test_that_users_are_redirected_to_error_url_when_invalid_subdomain_is_entered
  invalid_subdomain = "invalid-subdomain"

  auth_subdomain_url = URI(app_secrets.auth_app[:url].gsub(
   app_secrets.app_subdomain, invalid_subdomain))

  auth_app_url = app_secrets.auth_app[:url]

  host! test_domain(invalid_subdomain)
  get "/"

  assert_redirected_to auth_app_url
end

In the test we are expecting the application to redirect to auth_app_url but we are getting UnsafeRedirectError error for open redirections. In Rails 7 the new Rails defaults protects applications against the Open Redirect Vulnerability.

To allow any external redirects we can pass allow_other_host: true.

redirect_to <External URL>, allow_other_host: true

Since we use open redirection in many places we disabled this protection globally.

config.action_controller.raise_on_open_redirects = false

Argument Error for Mailgun signing key

After the upgrade, we started getting the following error in production.

>> ArgumentError: Missing required Mailgun Signing key. Set action_mailbox.mailgun_signing_key
in your application's encrypted credentials or provide the MAILGUN_INGRESS_SIGNING_KEY
environment variable.

Before Rails 7, we used the MAILGUN_INGRESS_API_KEY environment variable to set up the Mailgun signing key . In Rails 7, that is changed to MAILGUN_INGRESS_SIGNING_KEY. So we renamed the environment variable to fix the problem.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.