


























Everything that breaks upgrading Rails 7.2 to 8.1, and how to fix it: enum syntax, the Solid trifecta, Propshaft, params.expect, and every silent regression.
Ally Piechowski · · 8 min read
Rails 8 wants you to drop Redis, drop Sprockets, drop Sidekiq, and adopt a full stack of database-backed infrastructure. None of that is mandatory yet. The gap widens with every minor release.
The Rails 7.2 to 8.1 upgrade itself is one of the smoother major version jumps in recent Rails history. Fewer hard removals, fewer gotchas that prevent the app from booting. The ecosystem migration is where teams get stuck.
TL;DR: Breaking: enum syntax, to_time behavior, schema column reordering | New: Solid trifecta, params.expect, auth generator | Strategy: dual boot on main, not a branch
Rails 8.0 requires Ruby 3.2 minimum, and 3.4 is recommended for 8.1.
| Rails Version | Minimum Ruby | Recommended |
|---|---|---|
| 7.2 | 3.1 | 3.2+ |
| 8.0 | 3.2 | 3.3+ |
| 8.1 | 3.2 | 3.4+ |
If you’re going to Ruby 3.4, two things to watch:
csv was removed from Ruby’s standard library. If anything in your app or gems uses CSV parsing, add gem "csv" to your Gemfile explicitly. This surfaces as a confusing LoadError that doesn’t mention the Ruby version change.
it is now the default block parameter in Ruby 3.4 (like _1 but named). Any code using it as a block-local variable name will silently change behavior. RSpec’s it method is fine since it’s a method call, not a variable, but custom helpers or code inside spec blocks that use it as a local variable will bite you.
On your Rails 7.2 app:
# config/environments/test.rb
Rails.application.deprecators.behavior = :raise
Run your full test suite. The common ones you’ll hit:
enum defined with keyword arguments → use positional hash syntaxActiveRecord::ConnectionAdapters::ConnectionPool#connection → use #with_connection or #lease_connectionconfig.active_record.commit_transaction_on_non_local_return → removed in 8.0, decide your behavior nowActiveSupport::ProxyObject → gone, use BasicObject directlynil to form_with model: → use an explicit empty object or omit the argumentThese were warnings in 7.2. They’re NoMethodError or ArgumentError in 8.0.
bundle outdated
bundle exec bundle_report compatibility --rails-version=8.1
bundle exec bundle-audit check --update # security vulnerabilities
If you’re doing a broader codebase audit alongside the upgrade, here’s how I approach the Gemfile.
Gems that might break:
status: :unprocessable_entity becoming status: :unprocessable_content.mb_chars deprecation warnings on 8.2. Check their issues page for the latest compatibility status.REXML::ParseException on Rails 8. Update to latest.Net::HTTPSession is deprecated warnings on Ruby 3.4. Update.This is the one that bites the most codebases. Rails 8 removes the deprecated keyword argument syntax for enums:
# Before (removed in Rails 8):
enum status: { active: 0, archived: 1 }
enum status: { active: 0, archived: 1 }, _prefix: true
# After:
enum :status, { active: 0, archived: 1 }
enum :status, { active: 0, archived: 1 }, prefix: true
The first positional argument is now the enum name. The second is the mapping. Options like _prefix, _suffix, and _default drop their underscores.
If you have a lot of enums, this is the busywork of the upgrade. A find-and-replace gets you 90% of the way:
grep -rn "enum.*:" app/models/
Rails 8 sets Regexp.timeout to 1 second by default for ReDoS protection. Most apps won’t hit this, but if you do complex regex matching against user input, test your edge cases; they’ll raise Regexp::TimeoutError instead of hanging.
bin/rake stats: gone, use bin/rails statsBenchmark.ms: deprecated without an official replacement. Ruby’s Benchmark.realtime is the closest alternative:retries option: deprecated in favor of :timeoutrails/console/app, rails/console/helpers, and extending the console via Rails::ConsoleMethods are all removed in Rails 8.0. If you have console helpers in an initializer or a custom console setup, they’ll silently stop loading.
Move any console helpers to a plain Ruby module and include it manually, or use the console block in config/environments/development.rb:
console do
include MyConsoleHelpers
end
to_time Now Always Preserves Timezoneconfig.active_support.to_time_preserves_timezone is deprecated in Rails 8.1. The legacy false option is removed, and TimeWithZone#to_time now always preserves the receiver’s named timezone:
# Old behavior (to_time_preserves_timezone = false):
time_with_zone.to_time
# => converted to system local time
# New enforced behavior:
time_with_zone.to_time
# => preserves the named timezone
If your app has config.active_support.to_time_preserves_timezone = false, remove it and test any .to_time calls on TimeWithZone objects.
[foo]=bar in a query string used to parse as {"foo" => "bar"}. In Rails 8.1 it parses as {"[foo]" => "bar"}. If any API clients or forms submit parameter names starting with [, they’ll silently break: params just won’t match what the controller expects.
params.expectRails 8 introduces params.expect as the preferred replacement for params.require(:foo).permit(:bar). The old syntax still works (it’s not removed), but new Rails generators use expect, and it’s stricter about parameter structure.
# Before:
params.require(:post).permit(:title, :body, comments: [:message])
# After:
params.expect(post: [:title, :body, comments: [[:message]]])
params.expect validates the shape of the parameters, not just their presence. If someone sends post=123 instead of post[title]=foo, require would raise but expect handles this more consistently.
The double-array on comments isn’t a typo. [:message] means “a hash with a message key.” [[:message]] means “an array of hashes each with a message key.” params.expect requires you to be explicit about which you expect. Get it wrong and you’ll silently permit nothing.
params.expect returns an ActionController::Parameters object, same as permit. It’s designed to replace the full require(...).permit(...) chain, not be chained onto.
For new controllers, use expect. No need to migrate old ones all at once.
New Rails 8 apps get Propshaft instead of Sprockets. Existing apps keep Sprockets.
If you want to move to Propshaft, I wrote a full migration guide.
If your pipeline is stable, keep it. Add gem "sprockets-rails" to your Gemfile and move on.
Rails 8’s headline feature is database-backed replacements for Redis:
In most apps I audit, Redis exists solely to back Sidekiq and the cache store. Removing that dependency simplifies ops without sacrificing anything.
Your Sidekiq and Redis setups still work; none of this is forced.
When to adopt: If your app doesn’t need Redis-level throughput (most don’t), the Solid stack removes an entire infrastructure dependency. Basecamp and HEY run this in production.
When to wait: If you’re processing 100k+ jobs per minute or need sub-millisecond cache reads, stick with Redis. SSDs are fast, RAM is faster.
Migration strategy: Don’t switch everything at once. Start with Solid Queue for non-critical jobs, monitor, then move cache. Cable last, if at all.
bin/rails generate authentication scaffolds session-based auth with password resets for new apps. If you already have Devise, ignore it.
Active Record now sorts table columns alphabetically in schema.rb. Your first db:migrate after upgrading will produce a massive diff: every table’s columns get reordered.
This is purely cosmetic (it won’t change your database), but it will bloat your upgrade PR’s diff, cause merge conflicts with in-flight branches that touch migrations, and confuse reviewers.
Fix: Commit the schema.rb rewrite as its own isolated commit. Warn your team. In GitHub PR review, mark the schema.rb file as “Viewed” so reviewers can skip it. If you use structure.sql, column order is preserved; this only affects schema.rb.
Long-running jobs can now break into resumable steps, so they pick up where they left off after a deploy instead of restarting from scratch. Additive; doesn’t break existing jobs.
You can now mark associations as deprecated: true with :warn, :raise, or :notify modes. Useful for large codebases migrating off legacy associations.
Order-dependent finders (.first without .order), String#mb_chars, and the built-in Sidekiq adapter are all deprecated and will be removed in a future version.
Rails.application.deprecators.behavior = :raise in config/environments/test.rb and fix every warning.gem "sprockets-rails" to your Gemfile explicitly if you use Sprockets.gem "csv" to your Gemfile.The next_rails gem provides the next? helper for dual booting:
# Gemfile
if next?
gem "rails", "~> 8.1"
else
gem "rails", "~> 7.2"
end
Run CI against both. Merge fixes to main continuously. The upgrade happens on main, not on a branch.
config.load_defaults "7.2".bin/rails db:schema:dump immediately. This triggers the schema column reordering before any real migrations run, so you can commit it in isolation. Do this before anyone on the team runs a migration.Uncomment defaults from new_framework_defaults_8_1.rb one at a time. Each gets its own PR and deploy.
Migrate from Redis to Solid Queue/Cache/Cable. Migrate from Sprockets to Propshaft. Don’t mix them with the framework upgrade.
# Pre-upgrade checks (run on Rails 7.2)
bundle exec bundle_report compatibility --rails-version=8.1
bundle exec bundle-audit check --update
# railsdiff.org: diff framework defaults between 7.2 and 8.1
grep -rn "enum.*:" app/models/ # find old enum syntax
grep -rn "ProxyObject" app/ # removed in 8.0
grep -rn "read_encrypted_secrets" config/ # removed
# Post-upgrade essentials
bundle update rails
bin/rails app:update
bin/rails db:migrate
bin/rails test # or: bundle exec rspec
# Check for Regexp timeout issues
grep -rEn "Regexp\.(new|compile)" app/ lib/
If that branch has been sitting untouched for months, get in touch. This is what I do.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。