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

When you run rails app:update on a Rails 6.0 app, it creates a file with 16 new default configuration settings. Here’s what they do and how you should use them.

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

You’ll also want to make sure your app is stable on Rails 6.1 in production before getting started. A few of these flags are not backwards compatible with earlier Rails versions.

1. Rails.application.config.active_record.has_many_inversing = true

What does this do?

Consider the relationship between a post and its comments:

Starting with Rails 4, theinverse_of setting is implicit between these two models, allowing you to traverse them bidirectionally.

We should also be able to traverse the other way, right?

Uncommenting this flag would makepost.comments.first == comment return true. It fixes the ‘quirky’ behavior of being able to traverse one way, but not the other.

How to safely uncomment?

This flag can be safely uncommented.

Edge case: self-joins with the same name

Are you using recursive associations with the same name as the model? For example:

Currently this will have unexpected behavior:

Active Record will infinitely chain back-and-forth between these two records in an attempt to make their inverses traversable. There is an open PR that will make Rails throw an error instead.

If you are in this situation, I suggest renaming your assocation so that it no longer matches the model name:

It’s less confusing anyway.

2. Rails.application.config.active_storage.track_variants = true

What does this do?

Consider this user model that uses Active Storage to attach an avatar.

Our users upload high resolution avatars that look great on their user show page.

Now imagine that we’re building a users index page. We want to display 100x100 thumbnails of all avatars. It would use a ton of unnecessary bandwidth to serve so many high-res images. It would be great if we could serve a smaller version of our avatar just for the thumbnail.

Well we can, with Active Storage variants! Below we register our thumbnail variant with our avatar.

Now we can render them thusly:

How does this work? Does Active Storage pre-generate the thumbnail variant and store it in S3? No, here’s the process:

  1. Active Storage waits until the first request for the variant.
  2. Active Storage calls the S3 API to check if the variant is already present in the bucket.
  3. If it is present, it serves it. Otherwise it downloads the original image, uses ImageMagick to generate the variant, uploads the variant to S3, then serves it.

Now imagine our app has been in use long enough for all the variants to have already gone through this process. But our app is still performing step 2 for every thumbnail — that’s a lot of unnecessary API calls!

Moreover, checking for the existence of an image before uploading it triggers S3 eventual consistency. This means that our thumbnail is not guaranteed to be there when we serve its URL. It’ll just be there ‘eventually’. Oddly enough, if we didn’t do the check, we would have that guarantee.

Now back to this flag. Uncommenting it will tell Active Storage to track variants in our app’s own database. Now when a request comes in for a variant that’s already been generated, Active Storage doesn’t need to check with S3 before serving it. This eliminates those unnecessary API calls.

How to safely uncomment?

This flag can be safely uncommented once you’ve run the database migrations that make variant tracking possible. These migrations are generated by the same rails app:update command that created this new framework defaults file.

You may notice an N+1 issue if you are rendering many variants at once (e.g., displaying 20 thumbnails on an index page). This is because Active Storage replaces the S3 API call with a query on your app’s database.

Remember that this N+1 issue was already there, but now it’s N+1 Active Record queries instead of N+1 external API calls. That should still be a performance improvement.

Unfortunately there’s no way to fix this at present because Active Storage does not yet offer eager loading for variants. There is an open PR addressing this.

3. Rails.application.config.active_job.retry_jitter = 0.15

What does this do?

Consider this Active Job:

This job will fail once, then be requeued 3 times. Each requeue will be for 5 seconds in the future.

Uncommenting this flag will add a random 15% jitter to the wait time. Now each requeue will be for a time in the future ranging from 5 to 5*1.15 seconds.

Why would we want to add a random element to requeue wait times? Consider a job that fails not because it explicitly raises StandardError, but because a rate limiting error is raised when it calls an API. If we have hundreds of these jobs failing at once, they would all be requeued for the same time, only to all fail at once again.

Introducing a jitter would smooth out the execution time of these jobs, making it more likely that they don’t get rate limited. This is known as the thundering heard problem.

How to safely uncomment?

This flag can be safely uncommented.

Edge case: your jobs need to requeue for exactly their wait times.

As jobs are asynchronous, this is unlikely to be the case. But if you are in this situation, you’ll need to override this setting before uncommenting the flag:

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

What does this do?

Consider this Active Job:

Currently MyJob.perform(true)would log I just got enqueued! and I just got performed! despite the fact that aborting in the before_enqueue callback means that it was neither enqueued nor performed.

Uncommenting this flag will cause the job to log neither because neither after_ hook will be called. More generally, uncommenting this flag will prevent after_ hooks from being invoked if the job is aborted in the before_enqueue hook.

How to safely uncomment?

Grep your codebase for throw(:abort). If there are any instances of it, check to see if they’re inside before_enqueue hooks. If they are, check if those jobs also have after_enqueue or after_perform hooks.

If you don’t have any such jobs, this flag can be safely uncommented.

If you do, understand that there will be a behavior change with these jobs. Depending on what’s going on in your hooks, you might actually want this change! After all, why would you want those hooks called if they didn’t actually happen?

If you’re sure you want those actions to always happen, you can refactor your job not to use hooks:

Then you can safely uncomment this flag. Note that this will be the default behavior in Rails 6.2.

5. Rails.application.config.action_dispatch.cookies_same_site_protection = :lax

What does this do?

This flag controls the SameSite policy for your Rails cookies. What on Earth is a SameSite policy?

SameSite is an optional attribute for the Set-Cookie header. It allows servers to control whether the cookie being set gets included when other sites make browsers send cross-origin requests to those servers.

There are three possible values: none, strict, and lax. Let’s explore how a classic CSRF attack behaves with each value.

Here’s the SameSite=none scenario:

  1. Alice convinces Bob to visit her website, which initiates a cross-origin request to bank.com.
  2. bank.com originally set its cookie withSameSite=none, which has no restrictions on cross-origin requests. The browser includes the cookie.
  3. Bob happens to be logged in, so the request is authenticated, and the bank happily transfers Bob’s money to Alice.
  4. Bob types bank.com into his browser’s URL bar. The browser includes the cookie for this request as well.
  5. Bob sees that all his money is gone!

Here’s the SameSite=strict scenario:

  1. Alice convinces Bob to visit her website, which initiates a cross-origin request to bank.com.
  2. bank.com originally set its cookie with SameSite=strict, which tells the browser not to include its cookie on third-party requests from another site.
  3. Bob happens to be logged in, but since his cookie isn’t included, the request fails authentication. Bob’s money is safe.
  4. Bob types bank.com into his browser’s URL bar. The browser sees that it’s navigating across origins, so in accordance with SameSite=strict, it does not include the cookie for this request either. It turns out that strict is really strict!
  5. Bob is annoyed to find that he isn’t logged in! He refreshes the page. The browser sees that its top-level navigation is changing, but it’s not cross-origin this time. It includes the cookie. Bob sees that he is logged in now, but he’s confused. Why did he need to refresh the page to be logged in?

Here’s theSameSite=lax scenario:

  1. Alice convinces Bob to visit her website, which initiates a cross-origin request to bank.com.
  2. bank.com originally set its cookie with SameSite=lax, which tells the browser not to include its cookie on third-party requests from another site.
  3. Bob happens to be logged in, but since his cookie isn’t included, the request fails authentication. Bob’s money is safe.
  4. Bob types bank.com into his browser’s URL bar. The browser sees that its top-level navigation is changing origins, but in accordance with SameSite=lax, it doesn’t care. It includes the cookie since the user is initiating the request, not the third-party site.
  5. Bob sees that his money is safe. Bob is happy.

Although the strict scenario is an improvement on the none scenario, it was poor UX for Bob. The lax scenario allowed us to have our cake (blocking CSRF attacks) and eat it too (providing a logged-in experience when users navigate to our site across origins).

Now that we understand the different SameSite policies, which policy does Rails currently set? It doesn't! This is an optional attribute, remember?

Then which policy do browsers default to if no policy is specified? It depends on the browser. Chrome will default to lax, and most other browsers default to none (for now).

In order to standardize behavior across browsers, it makes sense to explicitly set a policy. Uncommenting this flag will include the SameSite=lax attribute on your Set-Cookie headers.

How to safely uncomment?

This flag is safe to uncomment. As you saw with the three scenarios, lax is a sensible default policy. The majority of your users probably use Chrome, which has already been defaulting to lax anyway.

Edge case: your app is a third-party tracker.

E.g., Facebook is a third-party tracker. It allows you to include a tracking pixel on your site. When a user loads your site, a request is sent for that pixel, including the user’s Facebook cookie. Facebook relies on that cookie to track users, so they can’t have it blocked.

Note that this is Facebook’s problem, not yours. If you’re merely using third-party tracking code, but are not yourself a tracker, you don’t have to worry about this.

But if your app is similar to Facebook in this way, you’re going to need different settings in development and production.

Aside from the obvious, force_ssladds the Secure attribute to your cookies. The Secure attribute prevents your cookie from being sent over HTTP. Browsers will reject SameSite=none unless it’s accompanied by the Secure attribute. Apparently sending cookies across origins in cleartext is just too insecure to allow.

You can let Rails use the default lax value in development.

6. Rails.application.config.action_controller.urlsafe_csrf_tokens = true

What does this do?

By default Rails includes a hidden authenticity token on forms as a CSRF countermeasure.

Here’s an example:

Let’s take a closer look at the value of that token:

This is a Base64-encoded nonce. Currently Rails generates it with SecureRandom.base64 and decodes it with Base64.strict_decode64.

Uncommenting this flag will make Rails instead generate it with SecureRandom.urlsafe_base64 and decode it with Base64.urlsafe_decode64.

Now it looks like this:

+ has been replaced with , / has been replaced with _, and = (padding) has been stripped.

Why this change? After-all, it doesn’t make a difference to Rails. As long as Rails can decode what it encodes, it will be able to verify the token.

There was concern that the former encoding (using+, /, and = characters) isn’t ‘websafe’. Consider trying to pass the token via a query string:

Rails itself passes authenticity tokens via POST params, not query strings. However, what if we start mixing technologies (e.g., a React frontend)? We might find ourselves in a situation where we need websafe tokens.

How to safely uncomment?

This flag can be safely uncommented.

Base64.urlsafe_decode64 can decode both types of encoding, making it forwards and backwards compatible.

7. ActiveSupport.utc_to_local_returns_utc_offset_times = true

What does this do?

ActiveSupport.utc_to_local_returns_utc_offset_times configures ActiveSupport::TimeZone.utc_to_local to return a time with a UTC offset instead of a UTC time incorporating that offset.— Rails docs

What does that mean? Consider this:

Bob, Jack, and Jill are all celebrating Y2K. Bob is in Iceland (UTC). Jack and Jill are in California (EST during the winter).

At midnight UTC Bob calls them up and asks what time it is in their local timezone:

“7 PM UTC”, Jack says.

“7 PM EST”, Jill says.

Jack is wrong. You can’t just do the timezone offset conversion (subtracting 5 hours) and then report the same timezone. That’s a totally different time. Besides, you didn’t ask him for UTC, you asked for his local timezone!

Jill is right. She does the timezone offset conversion and then reports the new timezone.

Currently utc_to_local is Jack. Flipping this flag makes utc_to_local Jill.

How to safely uncomment?

Grep your codebase for utc_to_local. This flag can be safely uncommented if there are no instances of it.

If there are instances of it, it’s likely that your app is Jack. You probably have behavior that relies on your app being Jack, so I would be hesitant to suddenly make it Jill.

I recommend carefully tracing what your app does with those times after casting them. If it’s just for display purposes (showing users some time in their local time), it would be safe to uncomment this flag.

But if your app is performing arithmetic on those times, especially comparing them against UTC times, you might be in for a big refactor (and a big headache).

8. Rails.application.config.action_dispatch.ssl_default_redirect_status = 308

What does this do?

It’s best practice to set config.force_ssl = true in production environments. This tells Rails to redirect HTTP requests to their HTTPS equivalents.

Currently Rails does this by responding to non-GET/HEAD HTTP requests with a 307 status. A 307 means “this has moved temporarily, here’s the new URL, but don’t change the request method (e.g., POST to GET)”.

Uncommenting this flag will make Rails respond with a 308 status instead. A 308 is the same as a 307, except it means that the resource has moved permanently, not just temporarily.

This allows for better caching and SEO.

How to safely uncomment?

This flag is safe to uncomment. The 308 status was introduced back in 2015, browser support is strong, and there are fallbacks in place.

Moreover, this change only applies to non-GET/HEAD requests. Most redirected HTTP requests are going to be GET (e.g., the user types in bank.com instead of https://bank.com).

9. Rails.application.config.active_record.legacy_connection_handling = false

What does this do?

Multiple database support is a major feature of Rails 6.0. Here’s an example of such a setup:

  1. database.yml lists two production databases: primary and replica.
  2. There is an ApplicationRecord model set to write to primary and read from replica.
  3. There is an AnimalRecord model set to write to primary and read from replica.
  4. There is a Person model that inherits ApplicationRecord.
  5. There is a Dog model that inherits from AnimalRecord.

Imagine that we’re writing a query involving both models:

But what if we know that for this query we definitely want to read from the primary? Rails 6.0 allows you to globally override the connection handling:

But what if we want to have User read from replica and Dog read from primary? We can’t in Rails 6.0! Not within the same query.

Granular connection swapping is a major feature of Rails 6.1. Here’s that query we wanted to write:

Uncommenting this flag enables granular connection swapping.

How to safely uncomment?

This flag is safe to uncomment.

The majority of applications should not need to make any other changes since the public APIs have the same behavior. — eileencodes

I know of no cases where this change would be problematic outside of apps that are accessing private functionality of ConnectionHandler.

Global connection overriding using ActiveRecord::Base.connected_to will continue to work.

10. Rails.application.config.action_view.form_with_generates_remote_forms = false

What does this do?

Rails has long provided form helpers. You used form_for when you were binding fields to a model, and form_tag when you weren’t. Frustratingly, the syntax was slightly different between them.

Rails 5.1 introduced form_with, a helper for working with both models and paths with a unified syntax.

form_with has an optional local kwarg which defaults to false. This makes your form ‘remote’, meaning that it submits via XHR instead of a normal synchronous POST.

Your XHR request is routed to the controller like normal:

This behavior is so unexpected that Rails itself refuses to use its own default.

The fact that forms in scaffolds are generated with local: true in Rails 5.2 looks like a smell to me. The vanilla code generated by Rails won't use its own defaults! — Jorge Manrubia

Uncommenting this flag will make form_with default to local: true. Forms will no longer be remote by default.

How to safely uncomment?

Grep your codebase for form_with. This flag can be safely uncommented if there are no instances of it.

If there are instances of it, check if they’re being passed local: true. If they are, you can leave them alone or remove the now redundant kwarg.

If they’re not being passed local at all, they’re currently remote forms. Assuming you want them to continue being remote forms—are you sure you’re handling render correctly?—pass them local: false.

As a matter of interest, this default might flip again. Turbolinks 6 is coming and will reportedly handle render as expected. I recommend always explicitly passing local to sidestep this default flip-flopping.

11. Rails.application.config.active_storage.queues.analysis = nil

What does this do?

When a file is attached with Active Storage, 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.

Historically these analysis jobs were enqueued on the default queue. Rails 6.0 new framework defaults moved them to the active_storage_analysis queue. This allowed apps to set a custom prioritization level for these jobs.

As I mentioned in my 6.0 article:

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 problem is that not everyone reads my articles. Those who don’t weren’t aware that they needed to add this queue, so their jobs were broken.

The Rails team decided to standardize on enqueueing everything on the defaultqueue in order to remove such footguns.

Uncommenting this flag will start enqueuingActiveStorage::AnalysisJobs on the default queue instead of their own dedicated queue.

How to safely uncomment?

This flag can be safely uncommented.

Afterward, you might want to delete the now dead active_storage_analysis queue. Make sure to allow any existing jobs enqueued on it to finish processing before doing so.

Edge case: your app relies on a custom prioritization level for ActiveStorage::AnalysisJobs.

For example, if your app processes critical jobs on default that you cannot afford to have blocked by low-priority analysis jobs.

If this is the case, you’ll need to override this setting before uncommenting the flag:

12. Rails.application.config.active_storage.queues.purge = nil

What does this do?

Consider this:

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? Active Storage doesn’t leave it to bloat your buckets. An ActiveStorage::PurgeJob is enqueued which will eventually get around to purging that file from S3.

Historically these purge jobs were enqueued on the default queue. Rails 6.0 new framework defaults moved them to the active_storage_purge queue. This allowed apps to set a custom prioritization level for these jobs.

As I mentioned in my 6.0 article:

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_purge queue to their config/sidekiq.yml.

The problem is that not everyone reads my articles. Those who don’t weren’t aware that they needed to add this queue, so their jobs were broken.

The Rails team decided to standardize on enqueueing everything on the defaultqueue in order to remove such footguns.

Uncommenting this flag will start enqueuingActiveStorage::PurgeJobs on the default queue instead of their own dedicated queue.

How to safely uncomment?

This flag can be safely uncommented.

Afterward, you might want to delete the now dead active_storage_purge queue. Make sure to allow any existing jobs enqueued on it to finish processing before doing so.

Edge case: your app relies on a custom prioritization level for ActiveStorage::PurgeJobs.

For example, if your app processes critical jobs on default that you cannot afford to have blocked by low-priority purge jobs.

If this is the case, you’ll need to override this setting before uncommenting the flag:

13. Rails.application.config.action_mailbox.queues.incineration = nil

What does this do?

Here’s (the relevant part of) how Action Mailbox handles incoming emails:

  1. Rails receives the raw email from an ingress.
  2. The raw email gets stored in Active Storage (e.g., S3).
  3. An InboundEmail record is created by parsing the raw email.
  4. An IncinerationJob is enqueued for 30 days in the future. When run, this job will purge the raw email from Active Storage and delete the parsed InboundEmail record. This ensures that PII isn’t permanently stored outside the domain model.
  5. The InboundEmail is routed to an ApplicationMailboxresponsible for processing it. The mailbox decides what data from the email it wants to permanantly store (e.g., creating a Message record).

These IncinerationJobs are currently enqueued on their own dedicated queue, action_mailbox_incineration.

Uncommenting this flag will start enqueing them on the default queue instead.

How to safely uncomment?

This flag can be safely uncommented.

Afterward, you might want to delete the now dead action_mailbox_incinerationqueue. Make sure to allow any existing jobs enqueued on it to finish processing before doing so. This could take up to 30 days.

Edge case: your app relies on a custom prioritization level for IncinerationJobs.

For example, if your app processes critical jobs on default that you cannot afford to have blocked by low-priority incineration jobs.

If this is the case, you’ll need to override this setting before uncommenting the flag:

14. Rails.application.config.action_mailbox.queues.routing = nil

What does this do?

Here’s (the relevant part of) how Action Mailbox handles incoming emails:

  1. Rails receives the raw email from an ingress.
  2. An InboundEmail record is created by parsing the raw email.
  3. A RoutingJob is enqueued. When run, this job will pass the InboundEmail to the appropriate ApplicationMailbox.

Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly accept new incoming emails without being burdened to hang while they're actually being processed. — Rails docs

These RoutingJobs are currently enqueued on their own dedicated queue, action_mailbox_routing.

Uncommenting this flag will start enqueing them on the default queue instead.

How to safely uncomment?

This flag can be safely uncommented.

Afterward, you might want to delete the now dead action_mailbox_routing queue. Make sure to allow any existing jobs enqueued on it to finish processing before doing so.

Edge case: your app relies on a custom prioritization level for RoutingJobs.

For example, if your app processes critical jobs on default that you cannot afford to have blocked by low-priority routing jobs.

If this is the case, you’ll need to override this setting before uncommenting the flag:

15. Rails.application.config.action_mailer.deliver_later_queue_name = nil

What does this do?

Consider this mailer:

UserMailer.welcome(1).deliver_later will enqueue this email to be sent asynchronously. Remember that under the hood this is just a normal ActiveJob.

Currently these email jobs are enqueued on the mailers queue.

Uncommenting this flag will start enqueing them on the default queue instead.

How to safely uncomment?

This flag can be safely uncommented.

Afterward, you might want to delete the now dead mailers queue. Make sure to allow any existing jobs enqueued on it to finish processing before doing so.

Edge case: your app relies on a custom prioritization level for async emails.

For example, if your app processes critical jobs on default that you cannot afford to have blocked by low-priority emails.

If this is the case, you’ll need to override this setting before uncommenting the flag:

16. Rails.application.config.action_view.preload_links_header = true

What does this do?

Imagine that we want to execute some JavaScript on a page when it’s finished loading. We might do something like this:

The page loads, then the script is downloaded, then the script is executed. Wouldn’t it be more performant if the browser could download the script while it’s parsing the HTML?

We can do that with a preload Link element:

Now the browser downloads the script at the same time that it’s parsing the HTML. Then the preloaded script is executed. That’s great, but the browser still has to parse the markup until it reaches our preload directive. Wouldn’t it be more performant if the browser knew to preload the script before it even looked at the HTML?

We can do that too, with a Link header:

That’s great, but littering our controllers with custom headers isn’t ideal. Wouldn’t it be convenient if Rails automatically included them for you?

That’s exactly what uncommenting this flag does! Uncommenting this flag will include Link header preload directives when you use javascript_include_tag or stylesheet_link_tag.

How to safely uncomment?

This flag can be safely uncommented. Browsers that support Link headers will get a performance boost. Browsers that don’t will ignore them.

Edge case: HTTP response header overflow

It's common to set config.assets.debug = true in the development environment. This will split concatenated assets into their constituent files and include them all separately. This may result in an exceptionally long Link header that exceeds the (effective) maximum 8192 bytes for HTTP response headers.

If this is causing problems for you, I recommend disabling this feature in development:

Edge case: IE 5–9 conditional assets

Does your site support IE 5–9? If so, you may be using IE conditional comments to ensure your IE-specific assets are only being loaded on IE.

Since Link headers aren’t aware of these comments, they can cause non-IE browsers to unnecessarily download IE assets. Note that they won’t be applied on those browsers, just preloaded. At worse this would unnecessarily consume bandwidth.

If you’re in this situation, your options are:

  1. Let non-IE browsers unnecessarily download some assets they won’t use.
  2. Disable this feature altogether.
  3. Stop supporting IE 5–9. What’s wrong with you?
  4. Write a helper that disables this feature on a per-asset basis (named something like javascript_include_tag_without_preload). Here’s an example.

Now what?

Once all 16 flags are uncommented, you don’t need config/initializers/new_framework_defaults_6_1.rb anymore. Delete that file and add load_defaults 6.1 to your config/application.rb. This is functionaly equivalent to having all the flags uncommented.

Would you like to work with the latest Rails?

The Bridge Services team at Evidation Health is hiring! We always work with the latest tech—not to mention competetitive pay and great WLB.

Passionate Rubyist with deep experience in ES6 and SQL.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store