Rails 6.0 new framework defaults: what they do and how to safely uncomment them

Lily Reile
10 min readOct 19, 2019

This is a walkthrough of the nine default flags in new_framework_defaults_6_0.rb generated by rails app:update. At the end of this article you’ll feel confident in deleting that file and adding load_defaults 6.0 to your application.rb.

This article assumes your application is on the 5.2 defaults. You can verify this be checking that load_defaults 5.2 is present in your application.rb.

1. Rails.application.config.action_view.default_enforce_utf8 = false

What does this do?

IE5 and its contemporaries introduced support for the accept-charset form attribute. This attribute tells the browser which encoding to use to encode submitted form data. Rails began automatically adding accept-charset="UTF-8" to force browsers to submit UTF-8.

However, IE5 had some odd behavior. IE5 ignored accept-charset if every submitted character could be expressed in the browser’s default charset. Consider a user who has their browser’s charset set to Latin-1. That user submits your form in IE5, but all of their input can be expressed in Latin-1. That user’s browser ignores the accept-charset attribute and submits the form encoded in Latin-1. You end up with Latin-1 in your UTF-8 database!

As a hack around this, the snowman HTML entity was added as a hidden form field back in 2010. Other encodings don’t support the unicode snowman emoji, forcing IE5 to respect the accept-charset attribute.

As a matter of interest, the snowman was later changed to the unicode checkmark character because people were concerned when snowmen started appearing in their logs!

To this day the checkmark remains as a hidden field in Rails forms. Uncommenting this flag will stop it from being included.

How to safely uncomment?

The weird accept-charset behavior exists in IE5 through IE8. You can safely uncomment this flag if you don’t support those browsers.

2. Rails.application.config.action_dispatch.use_cookies_with_metadata = true

What does this do?

Rails encrypts/signs cookie values. It decrypts/verifies the signature of those cookie values to ensure they weren’t modified by evildoers. However, this does not stop evildoers from copying the encrypted/signed values of some cookies and using them as the values for other cookies.

Imagine this scenario:

  1. Rails sets two cookies, is_admin with a value of false and is_a_doofus with a value of true.
  2. Evildoers swap the encrypted/signed values of the two cookies.
  3. Rails reads those values on request and says “ahh, yes, these values haven’t been modified”.
  4. Rails thinks that you’re a non-doofus admin when in fact you are supposed to be a doofus non-admin.

So back to this flag. Uncommenting it will cause Rails to embed a “purpose” field into cookies before encrypting/signing. Then in the above step 3 Rails would say “sure, these values haven’t been modified, but their purposes don’t match their cookie names”. The evildoer remains a doofus for another day.

How to safely uncomment?

Uncommenting this flag won’t break existing cookies. They will be read on request and rewritten with the purpose field on response. New cookies will have the purpose field moving forward.

# This option is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 6.0.

This warning looks scary, but don’t be alarmed. It just means that your app is locked into Rails 6.x once this flag is uncommented. Downgrading to Rails 5.x won’t be possible because it won’t understand the purpose field in cookies.

This flag is safe to uncomment once you’re confident that your app is stable on Rails 6.

3. Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false

What does this do?

HTTP responses include a content-typeheader. When Rails renders an HTML view, this looks like content-type: text/html. Note that the only value in this header is the media type, “text/html”.

Uncommenting this flag will add other values to the header. By default it will now include the charset: content-type: text/html; charset=utf-8.

The value of this header has historically been available for inspection at ActionDispatch::Response#content_type. This value will also change to include the charset.

How to safely uncomment?

Uncommenting this flag has an external effect and an internal effect.

The external effect is that consumers of your app will now get the full content-type header on responses. It’s safe to assume this isn’t a problem unless you know specifically that your consumers check against content-type.

The internal effect is that the value ActionDispatch::Response#content_type changes. Your test suite will indicate whether this affects you. If there are failures, they’re likely in old-school controller specs that look like this:

Failure/Error: expect(response.content_type).to eq("text/html");expected: "text/html"
got: "text/html; charset=utf-8"
(compared using ==)
# ./spec/controllers/users_controller_spec.rb:10:in `block (4 levels) in <top (required)>'

You have a few options for fixing them:

  • expect(response.media_type).to eq("text/html"). The media_type method returns just the media type like content_type did previously.
  • expect(response.content_type).to include("text/html").
  • Consider refactoring your controller tests into system tests. Controller tests are deprecated.

4. Rails.application.config.active_job.return_false_on_aborted_enqueue = true

What does this do?

I bet you know that ActiveJobs have lifecycle callbacks like before_enqueue. Did you know that throw(:abort) anywhere within an ActiveJob exits immediately without further processing? Did you know that you could do that within one of those callbacks?

class MyJob < ApplicationJob
before_enqueue { |job| throw(:abort) if job.arguments.first }
def perform; end
end
job1 = MyJob.perform_later(false)
job2 = MyJob.perform_later(true)

Regardless of this flag, job1 will be an instance of the enqueued job class. Currently job2 will also be an instance of the enqueued job class. Uncommenting this flag will make job2 be false instead since abort was thrown within its callback.

How to safely uncomment?

Grep for abort in your codebase. If you do have instances of abort, ensure that they don’t appear within job callbacks.

If they do appear in job callbacks, you’ll need to audit the places where those jobs are enqueued. Ensure that those places aren’t relying on perform_later to always be a job instance. If they do rely on it, you’ll need to refactor them to also accommodate false.

5. Rails.application.config.active_storage.queues.analysis = :active_storage_analysis

What does this do?

When a file is attached with ActiveStorage, the after_commit_create callback on ActiveStorage::Attachment is called. That callback enqueues an ActiveStorage::AnalysisJob. When performed, that job calls ActiveStorage::Blob#analyze. That method uses a plugin system to extract metadata from the file. That metadata is saved to the metadata column on the blob record.

The most common use of this is extracting heightand width data from images using mini_magick.

These ActiveStorage::AnalysisJobs are currently enqueued on the default queue. Uncommenting this flag will send them to their own dedicated active_storage_analysisqueue instead. This allows apps to set a custom prioritization level for these jobs.

How to safely uncomment?

Once this flag is uncommented, new ActiveStorage::AnalysisJobs will be enqueued on the active_storage_analysis queue. You’ll need to ensure your queuing backend is configured to process jobs on that queue.

E.g., apps using Sidekiq need to addactive_storage_analysis queue to their config/sidekiq.yml. The placement order will determine priority relative to the other queues.

This won’t break existing jobs that were already enqueued. They’ll continue to be processed on the default queue.

6. Rails.application.config.active_storage.queues.purge = :active_storage_purge

What does this do?

Consider this

class User < ApplicationRecord
has_one_attached :avatar
end
sam = User.create.avatar.attach(some_image_file)
sam.avatar.attach(different_image_file)

When the first file is attached, it gets uploaded to storage (e.g., S3) and Rails creates a record pointing to its storage location. When the second file is attached, it gets uploaded to S3 and Rails updates that record to point to the new storage location.

But what happens to the first file in S3? ActiveStorage doesn't leave it to bloat your buckets. An ActiveStorage::PurgeJob is enqueued which will eventually get around to purging that file from S3.

These ActiveStorage::PurgeJobs are currently enqueued on the default queue. Uncommenting this flag will send them to their own dedicated active_storage_purgequeue instead. This allows apps to set a custom prioritization level for these jobs.

How to safely uncomment?

Once this flag is uncommented, new ActiveStorage::PurgeJobs will be enqueued on the active_storage_purgequeue. You’ll need to ensure your queuing backend is configured to process jobs on that queue.

E.g., apps using Sidekiq need to add active_storage_purgequeue to their config/sidekiq.yml. The placement order will determine priority relative to the other queues.

This won’t break existing jobs that were already enqueued. They’ll continue to be processed on the default queue.

7. Rails.application.config.active_storage.replace_on_assign_to_many = true

What does this do?

Consider this:

class Message < ApplicationRecord
has_many_attached :uploads
end
files = get_array_of_files
message = Message.create(uploads: files)
files << get_another_file
message.update(uploads: files)

When the message is created, ActiveStorage uploads the files to storage (e.g., S3). But what should happen when the uploadsarray is reassigned on that last line?

  1. Rails diffs the existing array with the new array. It uploads new files and ignores files that were already in the existing array.
  2. Rails doesn’t do any diffing. It purges all the existing files and uploads all the files in the new array.

Currently Rails does 1. Uncommenting this flag makes Rails do 2 instead.

How to safely uncomment?

Grep your codebase for has_many_attached. That will identify the models affected by this change. If no models are affected, you can safely uncomment this flag.

If models are affected, it’s recommend that you refactor anywhere you’re using reassignment to use attach instead. Instead of touching existing files, attach naively attaches the files passed to it. This means that you’ll need to add your own logic for preventing duplicates if that is import for your app.

8. Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"

What does this do?

ActionMailer::DeliveryJob is currently the job that is enqueued when deliver_later is called on a Rails mailer.

The Rails team wanted to introduce a breaking change to that ActionMailer::DeliveryJob class. However, that would break existing jobs during the upgrade process. So they decided to make a new class, ActionMailer::MailDeliveryJob.

Uncommenting this flag will enqueue mailer jobs with that new ActionMailer::MailDeliveryJob class moving forward.

The plan is that existing jobs will continue processing with ActionMailer::DeliveryJob while new jobs are enqueued with ActionMailer::MailDeliveryJob. Since no new jobs will be enqueued with ActionMailer::DeliveryJob, eventually that class won't be needed. In fact, the Rails team will be deleting ActionMailer::DeliveryJob entirely in Rails 6.1.

How to safely uncomment?

The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
If you send mail in the background, job workers need to have a copy of
MailDeliveryJob to ensure all delivery jobs are processed properly.
Make sure your entire app is migrated and stable on 6.0 before using this setting.

This warning looks scary, but don’t be alarmed. It just means that your app is locked into Rails 6.x once this flag is uncommented. Downgrading to Rails 5.x won’t be possible because Rails 5.x doesn’t have theActionMailer::MailerDeliveryJob class.

This flag can be safely uncommented once you’re confident that your app is stable on Rails 6.

9. Rails.application.config.active_record.collection_cache_versioning = true

What does this do?

Rails 5.2 introduced recyclable cache keys. This feature moved the volatile updated_at portion of an active record’s cache_key to its cache_version.

# In Rails 5.1
user = User.last
user.cache_key # "users/281-20191007212244313194"
user.touch
user.cache_key # "users/281-20191017003012868191"
# In Rails 5.2
user = User.last
user.cache_key # "users/281"
user.cache_version # "20191007212244313194"
user.touch
user.cache_key # "users/281"
user.cache_version # "20191017003012868191"

You can see that the cache key did not change in Rails 5.2 like it did in Rails 5.1. Instead of relying on the key changing to cause a cache miss, Rails 5.2 reuses the key and updates its cache entry. This results in fewer cache misses, increasing performance.

Uncommenting this flag brings recyclable cache keys to collections (i.e, ActiveRecord::Relation) as well.

Rails.application.config.active_record.collection_cache_versioning = false
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e-281-20191007212244313194"
Rails.application.config.active_record.collection_cache_versioning = true
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e"

How to safely uncomment?

This flag can be safely uncommented. I glossed over some of the intricacies for two reasons.

Firstly, this change is a concern for cache adapters (e.g, Redis), not user code. I don’t know of any modern adapters that don’t support it.

Secondly, there are already excellent articles from Big Binary on recyclable cache keys and recyclable collection cache keys.

10. config.autoloader = :zeitwerk

What does this do?

This is the secret 10th flag that takes effect with load_defaults 6.0, but isn’t in the new_framework_defaults file. This flag will switch the Rails autoloader from classic to Zeitwerk.

If you’d like to switch over before loading the new defaults, you can call config.autoloader = :zeitwerk in application.rb.

How to safely uncomment?

The migration guide is worth a read, but in practice I found only one pain point. Zeitwerk removes support for autoloading within initializers.

When running your tests, look for a warning like this:

DEPRECATION WARNING: Initialization autoloaded the constant Foo.Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.
Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload Foo, for example,
the expected changes won’t be reflected in that stale Class object.
These autoloaded constants have been unloaded.

What’s the issue?

Consider these two files:

# app/foo.rbclass Foo
def self.bar?
true
end
end

and

# config/initializers/baz.rb
BAZ = Foo

In classic mode, Rails will autoload Foo without complaint when it is encountered in the initializer. BAZ.bar? will return true as expected.

What happens if foo.rb is edited to return false instead? BAZ.bar? will still return true. This is because initializers are not re-run when code is reloaded. The value of Foo is stuck until the application is restarted.

Zeitwerk doesn’t let you assemble this particular footgun. If it detects autoloading during initialization, it unloads the constant and throws this warning.

What’s the solution?

The solution is to remove the autoloading. If Foo is only referenced in a single initializer, you can simply move its definition to that initializer:

# config/initializers/baz.rbclass Foo
def self.bar?
true
end
end
BAZ = Foo

If it’s referenced in multiple places, move foo.rb to a directory that isn’t autoloaded (e.g.,lib). Then explicitly require it:

# config/initializers/baz.rbrequire Rails.root.join('lib', 'foo')
BAZ = Foo

You’re safe to use Zeitwerk once that warning is gone and your tests are green.

Edit (3/30/2021)

Rails introduced a new rake task, bundle exec rails zeitwerk:check , that will autoload all files in your app in order to verify Zeitwerk compatability. Run this task after switching to Zeitwerk.

Running this task surfaced two additional painpoints.

Namespace doesn’t match file nesting

Unable to load application: Zeitwerk::NameError: expected file /foo/bar.rb to define constant Foo::Bar, but didn't

If we check /foo/bar.rb, we find:

# /foo/bar.rb
class Bar
# ...
end

Since bar.rb is nested within the foo folder, Zeitwerk expects the Bar class therein to be similarly nested within the Foo namespace. You can resolve this by adding the namespace:

# /foo/bar.rb
class Foo::Bar
# ...
end

Be careful to grep your codebase for other uses of Bar that need to be qualified with Foo::Bar!

Mismatched acronym inflection

Unable to load application: Zeitwerk::NameError: expected file /csv.rb to define constant Csv, but didn't

If we check /csv.rb, we find:

# /csv.rb
class CSV
# ...
end

By default Zeitwerk maps lowercase ruby files (e.g., csv.rb) to their capitalized constants (e.g., Csv). This doesn’t work very well for acronyms.

You can of course find/replace CSV to Csv throughout your project. However, if you’d prefer to keep your UPPERCASE acronym, you can register it with the ActiveSupport inflector.

# config/initializers/inflections.rb
# These inflection rules are supported but not enabled by default:
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'CSV'
end

--

--