280 Support for modern Ruby versions up to Ruby 3.3

Posted . Visible to the public.

We have made all versions of Rails LTS compatible with Ruby 3.3 or below. All Rails components should work as expected with no deprecation warnings.

However, upgrading Ruby will require manual effort. Your application may contain code that does not work on the latest Ruby. It is likely that some of your third-party dependencies do not work on the latest Ruby. The upgrading steps vary for every Rails application, and increase with the number of third-party gems.

We have ourselves successfully upgraded several older applications. Usually, the required changes to the application code are minimal and in most cases it was possible to find compatible versions of all gems. In rare cases however, we did need to use monkey patches or fork a gem.

Upgrading Ruby should still be significantly easier than doing a full Rails upgrade.

Why you would upgrade

There are a few reasons to use a newer Ruby version:

  • Ruby versions are only under active maintenance for roughly 3 years after release. Older Rubies do have a few known security issues such as buffer overreads. While these vulnerabilities are usually difficult to abuse in the context of a Rails application, they do exist.
  • Newer Rubies are faster. This is most pronounced when you upgrade from Ruby 1.8 (expect a 2x - 3x performance boost) or early 2.x versions.
  • It might be difficult to install older Ruby versions on modern operating systems. This is mostly an issue for Ruby < 2.5.

Compatibility matrix

Rails LTS is tested only against a subset of Ruby versions (decreasing the maintenance effort on our part).
We recommend to use one of these combinations:

Rails 2.3 LTS Rails 3.2 LTS Rails 4.2 LTS Rails 5.2 LTS
Ruby 1.8.7
Ruby 1.9.3
Ruby 2.1.x
Ruby 2.2.x
Ruby 2.3.x
Ruby 2.5.x
Ruby 2.7.x
Ruby 3.1.x
Ruby 3.3.x

How to upgrade Ruby

If you decide to upgrade Ruby across major versions, we recommend doing the upgrade in steps.

  • When starting from Ruby 1.8.7, upgrade to 2.5 first
  • When upgrading to Ruby 3.x, upgrade to 2.7 first

During each upgrade round, follow this workflow:

  1. Update to the latest patch level of your Rails LTS version.
  2. Update your Ruby version.
  3. Follow the tips and instructions in this card for your LTS version, gems, and Ruby version.
  4. Run bundle install.
  5. Try to get rails console (or script/console) running without errors.
  6. Try to get rails server (or script/server) running without errors.
  7. Run your tests and fix remaining errors.

Notes for Rails 2.3 LTS

  1. If you haven't already, you need to switch to Bundler.
  2. Add gem 'test-unit', '= 1.2.3', require: false to your Gemfile as a top-level gem (not inside the group :test).
  3. In config/preinitializer.rb, add:
    Encoding.default_external = Encoding::UTF_8
    
  4. Check script/server, script/console etc. The first require line should look like this:
    require File.expand_path('../../config/boot',  __FILE__)
    

Notes about gems

If you see errors produced by third party gems (usually recognizable in the stacktrace), we recommend going to rubygems.org, searching for the gem and looking for the latest gem version that supports your Rails major version (e.g. if you are on Rails 3, find the latest gem that does not require activerecord ~> 4 etc.).

We have some explicit notes for a few common gems:

mysql / mysql2

If you're using mysql, please switch to the mysql2 gem. To do this, add it to your Gemfile, and set your database adapter to mysql2 in your database.yml.

mysql2 is mostly a drop-in replacement for mysql. The main difference is that you might sometimes get casted values (i.e. Time objects instead of strings) when you use low-level methods in ActiveRecord, such as select_values.

The following mysql2 version are known to work:

  • 0.5.4 on Ruby 2.5
  • 0.5.6 (latest at time of writing) on all newer Rubies

Note that we used to offer a fork for mysql2. This is no longer recommended, and the 0.5.x versions of mysql2 do work even on the latest Rails 2.3 LTS.

pg

All versions of Rails LTS and all Rubies >= 2.5 should work with pg version 1.5.6 (latest at time of writing).

i18n

When upgrading to Ruby 3+, please upgrade i18n to at least version 1.9.

rake

When upgrading to Ruby 3.3+, please upgrade rake to at least version 13.

rspec 1.x

If your application still uses a 1.x version of rspec, we recommend upgrading to our fork of RSpec 1 Show archive.org snapshot like this:

gem "rspec", git: 'https://github.com/makandra/rspec.git', branch: '1-3-lts'
gem "rspec-rails", git: 'https://github.com/makandra/rspec-rails.git', branch: '1-3-lts'

therubyracer

We recommend to replace this with the mini_racer gem. You may also have to update the execjs gem to allow this.

date-performance

This gem is no longer required, please remove it.

fastercsv

Please drop this gem. Instead, require 'csv', which has the same API.

Upgrading from Ruby 1.x to Ruby 2+

YAML parser changes

The YAML parser has changed from Syck to Psych. Some .yaml files need to be fixed for Psych. One common case is found in default locale files. Instead of

order: [:day, :month, :year]

use

order:
- :day
- :month
- :year

You also now need to quote strings that start with * or &.

Finally, if you made use of ActiveRecord's serialize feature, you might want to check that serialized data in your database can still be loaded.

Lambdas

lambdas (but not procs) have started to enforce their arity. You can no longer call a lambda with arguments if the block does not take any.

Fix this by switching the offending (or possibly all) lambdas to procs.

Changes to Ruby's standard library

  • object.respond_to?(:a_protected_method) used to be true, but is now false. You can use object.respond_to?(:a_protected_method, true) instead (which will also be true for private methods).
  • object.id no longer aliases object.object_id
  • Array("line 1\nline 2") no longer splits on linebreaks. Use "line 1\nline 2".lines instead if you used Array for that purpose.
  • Array#to_s used to work like Array#join, but now works like Array#inspect. Use Array#join explicitly.
  • "some words".each is gone. Use "some words".split.each.
  • A few methods no longer accept symbols instead of strings ("foo".starts_with?(:f) is now an error).
  • iconv is no longer in the standard library. You can add it as a gem, or better replace it with String#force_encoding / String#encode:
    # old
    converter = Iconv.new('UTF-8//IGNORE', 'WINDOWS-1252')
    converter.iconv(text)
    
    # new
    text.force_encoding('WINDOWS-1252').encode('UTF-8', undef: :replace, invalid: :replace)
    

String encoding

You can potentially run into issues with String encoding. In general, everything should always be encoded as UTF-8. If you deal with binary data or lowlevel operations (like String#unpack), you must potentially use String#force_encoding or String#encode.

Upgrading from Ruby < 2.5 to Ruby 2.5+

Ruby 2.4+ deprecates Fixnum and Bignum in favor of a unified Integer class. This "only" causes deprecation errors, since Fixnum internally resolves to Integer.

If a third-party gem uses Fixnum and you want to resolve the deprecation errors, you can often manually define a Fixnum class in the gem's namespace. For example, we've added the following initializer to one of our projects:

# config/initializer/fixnum_deprecation_fixes.rb

# will_paginate
WillPaginate::Fixnum = Integer
BootstrapPagination::Fixnum = Integer

# axlsx
Axlsx::Fixnum = Integer

Upgrading from Ruby 2.x to Ruby 3

Ruby 3 introduces many breaking changes, mainly by changing the APIs of some standard library methods and by changing how keyword arguments work.

The ruby-3-backward-compatibility gem

To address some issues in Ruby 3, we created the ruby-3-backward-compatibility gem Show archive.org snapshot . Please add it to the top of your Gemfile (Rails LTS will require it automatically).

It mostly adds back some removed APIs in Ruby. For example:

# old API
ERB.new(template, nil, '-', '@output_buffer')
# new API
ERB.new(template, trim_mode: '-', eoutvar: '@output_buffer')

# old API
YAML.safe_load(yaml, [Time], [], true, 'foo.yml')
# old API
YAML.safe_load(yaml, permitted_classes: [Time], permitted_symbols: [], aliases: true, filename: 'foo.yml')

The ruby3-backward-compatibility gem patches all of these, so you should not run into problems and do not have to change your code.

YAML safety

As you probably know, loading user-controlled YAML is dangerous. Exploits work by serializing internal Ruby objects to YAML, which can cause code execution when getting de-serialized.

This is why YAML introduced YAML.safe_load. It used to work like this:

  • Calling YAML.safe_load on dangerous YAML content would produce errors.
  • Calling YAML.load or YAML.unsafe_load would produce no errors and must not be called with unsanitized user input.

When upgrading to psych version >= 4 (the "yaml" gem; which is default for Ruby 3), this behavior changes as follows:

  • Calling YAML.load or YAML.safe_load on dangerous YAML content will produce errors.
  • Calling YAML.unsafe_load produces no errors. You must not call it with (unsanitized) user input.

Any code (including third-party gems) that is unaware of this change and uses YAML.load instead of YAML.unsafe_load will start producing errors. For example, i18n versions < 1.9 will crash when loading your locale files.

This is something we cannot address globally, as there is no way of knowing which instances of YAML.load are fine to load dangerous content, and which are not.

Hence, you need to manually change YAML.load to YAML.unsafe_load where appropriate.

Keyword arguments

Ruby 3 no longer allows to call methods expecting keyword arguments with a hash. Example:

def method_with_kwargs(param, keyword_param: 'default'); end

# no longer works
method_with_kwargs('foo', { keyword_param: 'bar' })

# works as before
method_with_kwargs('foo', keyword_param: 'bar')
method_with_kwargs('foo', :keyword_param => 'bar')
method_with_kwargs('foo', **{ keyword_param: 'bar' })

# the inverse (converting keyword to an options hash) still works fine
def method_with_options_hash(params, options = {}); end
method_with_options_hash('foo', bar: 'baz')

Unfortunately, the invalid invocations happen all over the place. To simplify the fix, the ruby3-backward-compatibility gem introduces a callable_with_hash helper:

def method_with_kwargs(param, keyword_param: 'default'); end

extend Ruby3BackwardCompatibility::CallableWithHash
callable_with_hash :method_with_kwargs

# now this works again
method_with_kwargs('foo', { keyword_param: 'default' })

callable_with_hash works by wrapping the original method with a version that accepts both an options hash or keyword arguments.

You can even use this on external APIs:

# i18n now requires keyword args, so this fails
I18n.t('user', { scope: ['models'] })

# This makes it work again (this is already included in the ruby3-backward-compatibility gem)
module I18n
  extend Ruby3BackwardCompatibility::CallableWithHash
  callable_with_hash :t
end

The ruby3-backward-compatibility gem applies callable_with_hash to a significant number of Rails methods, so you should mostly be good calling Rails methods with either style.

Delegation

If your code uses some custom delegation (or method_missing) style code, you will have to take care of keyword arguments as well.

class Foo < Base
  # this does not work with keyword args
  def method1(*args)
    super
  end
    
  # this will
  def method2(*args, **kwargs)
    super
  end
  
  # as will this
  def method3(...)
    super
  end
end

If for some reason you have to write code that also works with some legacy ruby versions, find even more details here.

Upgrading to Ruby 3.3+

ruby3-backward-compatibility gem

Please update to the latest version of the ruby3-backward-compatibility gem.

mysql2

In case you are still using our mysql2 fork, please upgrade to the latest official release (see above).

Default gems

Ruby 3.3 will print a lot of warnings about gems being removed from the list of default gems (such as csv, bigdecimal etc.).

If you see such a warning, just add the gem to your Gemfile.

Tobias Kraze
Last edit
Tobias Kraze
License
Source code in this card is licensed under the MIT License.
Posted by Tobias Kraze to Rails LTS documentation (2024-05-07 11:01)