Skip to content

Ruby 4.0 compatibility: CGI.parse removed in LoginProtection#return_address_with_params #2057

Description

@bvogel

Summary

LoginProtection#return_address_with_params calls CGI.parse which was removed from Ruby's standard library in Ruby 4.0. This causes a NoMethodError on every OAuth callback, making authentication completely broken on Ruby 4.0.

Affected versions

Confirmed broken in shopify_app 22.5.2 and 23.0.1 (per the source). Ruby 4.0.1.

Location

lib/shopify_app/controller_concerns/login_protection.rb

def return_address_with_params(params)
  uri = URI(base_return_address)
  uri.query = CGI.parse(uri.query.to_s)       # ← NoMethodError on Ruby 4.0
    .symbolize_keys
    .transform_values { |v| v.one? ? v.first : v }
    .merge(params)
    .to_query
  uri.to_s
end

Root cause

CGI was separated from Ruby's stdlib into a standalone gem in Ruby 3.4 (with a deprecation warning) and fully removed in Ruby 4.0. See ruby/cgi#82.

Suggested fix

Replace with Rack::Utils.parse_query, which is already available as a transitive dependency of Rails, returns a plain hash directly (no array-unwrapping needed), and is idiomatic for query string parsing in a Rack/Rails context:

def return_address_with_params(params)
  uri = URI(base_return_address)
  uri.query = Rack::Utils.parse_query(uri.query.to_s)
    .symbolize_keys
    .merge(params)
    .to_query
  uri.to_s
end

This is cleaner than the original because Rack::Utils.parse_query returns string values directly (not arrays), so the .transform_values unwrapping step is no longer needed.

Alternatives considered

  • Add gem 'cgi' to the gemspec: Would restore CGI.parse without code changes, but adds an external dependency purely to keep an awkward API that wraps every value in an array.
  • URI.decode_www_form(...).to_h: Pure stdlib, no extra deps, but more verbose than the Rack alternative.

Workaround

Apps running Ruby 4.0 can monkey-patch the method in an initializer:

# config/initializers/shopify_app_ruby4_compat.rb
module ShopifyApp
  module LoginProtection
    private

    def return_address_with_params(params)
      uri = URI(base_return_address)
      uri.query = URI.decode_www_form(uri.query.to_s).to_h
                    .transform_keys(&:to_sym)
                    .merge(params)
                    .to_query
      uri.to_s
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions