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 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.
Comments
No comments yet. Be the first.