Rails 6.0 new framework defaults: what they do and how to safely uncomment them
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:
- Rails sets two cookies,
is_admin
with a value offalse
andis_a_doofus
with a value oftrue
. - Evildoers swap the encrypted/signed values of the two cookies.
- Rails reads those values on request and says “ahh, yes, these values haven’t been modified”.
- 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-type
header. 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")
. Themedia_type
method returns just the media type likecontent_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
endjob1 = 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 height
and width
data from images using mini_magick
.
These ActiveStorage::AnalysisJob
s are currently enqueued on the default
queue. Uncommenting this flag will send them to their own dedicated active_storage_analysis
queue instead. This allows apps to set a custom prioritization level for these jobs.
How to safely uncomment?
Once this flag is uncommented, new ActiveStorage::AnalysisJob
s 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
endsam = 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::PurgeJob
s are currently enqueued on the default
queue. Uncommenting this flag will send them to their own dedicated active_storage_purge
queue instead. This allows apps to set a custom prioritization level for these jobs.
How to safely uncomment?
Once this flag is uncommented, new ActiveStorage::PurgeJob
s will be enqueued on the active_storage_purge
queue. 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_purge
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.
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
endfiles = 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 uploads
array is reassigned on that last line?
- Rails diffs the existing array with the new array. It uploads new files and ignores files that were already in the existing array.
- 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
endBAZ = 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