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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions