LaunchKit
← All posts
· 6 min read · by The LaunchKit team · 1 views

Most of Rails 8.2's new defaults can't be rehearsed on 8.1

The standard upgrade advice is sound: copy new_framework_defaults_8_2.rb in, uncomment one line a week, fix what breaks, and the version bump becomes a non-event. We went to do that for 8.2 and found that most of the lines have nothing behind them yet, and that Rails doesn't say so.

Measured on 2026-10-20 against rails/rails main and a Rails 8.1.4 app.

Twelve now, eleven three weeks ago

The when "8.2" branch in railties/lib/rails/application/configuration.rb makes twelve assignments today:

action_controller.forgery_protection_verification_strategy = :header_only
action_controller.default_protect_from_forgery_with        = :exception
action_controller.rescue_from_event_backtrace              = :array
action_dispatch.strict_accept_header                       = true
action_dispatch.default_headers                            = { ...four keys... }
action_view.erb_implementation                             = :herb
active_record.postgresql_adapter_decode_bytea              = true
active_record.postgresql_adapter_decode_money              = true
active_storage.analyze                                     = :immediately
active_storage.s3_public_uploads_via_acl                   = false
active_job.enqueue_after_transaction_commit                = true
ActiveSupport.raise_on_invalid_time_zone_parse             = true

On 26 September the same branch made eleven, which is what Rails 8.2, read from the source counted at the time. active_storage.s3_public_uploads_via_acl has appeared since, which is worth knowing for its own sake and worth knowing as a property of the thing: this list is not frozen, and a rehearsal you finish today may be a line short next month.

Meanwhile RubyGems still has no 8.2 of any kind, prerelease included, and the newest publication on any Rails line is 8.1.4 from 24 September. There's no beta to read, so main is the only source, and when 8.2 is coming out is still answered by a cadence rather than a date.

Nine of them have nothing to turn on

Here's what those settings look like from inside a Rails 8.1.4 application:

On 8.1.4
ActionController::Base.respond_to?(:forgery_protection_verification_strategy) false
ActionController::Base.respond_to?(:default_protect_from_forgery) false
ActionController::Base.respond_to?(:rescue_from_event_backtrace) false
ActiveStorage.respond_to?(:analyze) false
ActiveStorage.respond_to?(:s3_public_uploads_via_acl) false
ActiveSupport.respond_to?(:raise_on_invalid_time_zone_parse) false
ActionDispatch::Request#strict_accept_header false
ActiveJob::Base.enqueue_after_transaction_commit false, and real
ActionDispatch::Response.default_headers five keys, and real
ActionView::Template::Handlers::ERB.erb_implementation ...ERB::Erubi, and real

The two PostgreSQL decode flags aren't in that table because the test app runs SQLite, so they weren't probed and I'm not going to guess at them.

So roughly three of the twelve can be exercised today. For the rest there is no method, no deprecation, and no warning, because the code that would read the setting hasn't been written into a released version yet.

The part that makes this a trap

You can still write all twelve into config/application.rb, and Rails will accept every one.

Rails.application.config.action_controller.total_nonsense       # => nil
Rails.application.config.action_controller.total_nonsense = true
Rails.application.config.action_controller.total_nonsense       # => true

That's an invented key. config.action_controller is an ActiveSupport::OrderedOptions, which answers nil for anything it hasn't got and stores anything you give it. No NoMethodError, no typo detection, no distinction between a setting Rails will read and one nobody will ever look at.

Which means a line that does nothing is indistinguishable from a line that works, by inspection and by any check you could write against the config object. Uncomment config.active_storage.analyze = :immediately on 8.1, deploy, watch nothing break, and tick it off. You haven't tested anything. You've stored a value.

The twelve assignments made by config.load_defaults 8.2 in rails main, each checked against a Rails 8.1.4 application to see whether an API exists to turn it on. Three are live and can be exercised today: Active Job's enqueue after transaction commit, which reads false, Action Dispatch's default headers, which currently carries five keys including X-XSS-Protection, and Action View's ERB implementation, which reads Erubi. Seven are confirmed absent, with the consuming object answering false to respond_to for each, and two PostgreSQL adapter flags were not probed because the test app uses SQLite. A panel underneath shows an invented configuration key being assigned and read back successfully, demonstrating that OrderedOptions cannot tell you which of the twelve lines does anything.

The three you can actually rehearse

Worth doing now, because each is a real behaviour change and none of them needs 8.2.

enqueue_after_transaction_commit. It reads false on 8.1.4 and has existed since 7.2, so setting it to true today does exactly what 8.2 will do to you. It moves every perform_later to after the transaction commits, which fixes the race where a worker beats the commit and can also surface jobs you were relying on being enqueued inside a transaction that then rolled back. That second group is the thing to go looking for, and it's the subject of after_commit, and the job that runs before the row exists.

default_headers. Today's five keys are X-Frame-Options, X-XSS-Protection, X-Content-Type-Options, X-Permitted-Cross-Domain-Policies and Referrer-Policy. 8.2's hash has four: X-XSS-Protection is gone, which is right, because that header has been deprecated by browsers for years and in some configurations introduced a vulnerability of its own. Setting the four-key hash on 8.1 is a one line change you can ship and watch.

erb_implementation. 8.1.4 reads ActionView::Template::Handlers::ERB::Erubi and 8.2 wants :herb. The accessor is live, so the swap is expressible; whether Herb slots cleanly into an 8.1 app wasn't tested here, and that's the one of the three with real homework attached.

What to do with the other nine

Not nothing, and not a config line either.

Each of them describes a behaviour you can go and find in your own codebase today. Time.zone.parse returning nil on garbage is a grep, and every call site that relies on the nil is a future exception. Active Storage analyzing before validation rather than after is a question about your attachment validations, answerable by reading them. The CSRF verification strategy change is answerable by knowing whether anything posts to you without a token, which Can't verify CSRF token authenticity is about.

That's the honest version of rehearsing an unreleased release: the config switches are mostly not available, so the preparation is reading your own code against a list of named behaviours rather than toggling flags.

What this page doesn't cover

The two PostgreSQL adapter flags, because the app used here is on SQLite. decode_bytea and decode_money change what the adapter hands back for those column types, which is the kind of change that matters enormously to the handful of apps using them and not at all to everyone else, and somebody on Postgres should check them properly.

Nothing here runs an 8.2 app, because there isn't one to run. Every claim about what 8.2 does is read from main rather than observed, and main moves, as the twelfth assignment demonstrates. The 8.1.4 measurements are the solid half.

#rails #upgrades

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.