Reason #93 • April 3rd, 2026

Responsible monkey patching with Module#prepend

As discussed earlier this week, changing the behaviour of existing code via monkey patching is generally inadvisable, but there are cases where it's the most convenient solution to a problem.

There are more elegant ways to override methods than just reopening the class and redefining them, though. By leveraging Module#prepend we can insert a module into the ancestry chain of the class we want to modify. This has the benefit of allowing us to call the original method via super and also makes it clearer that we're doing something out of the ordinary:

Ruby
module ChompingLines
  # NOTE: changes the default value of chomp to true
  def lines(record_separator = $/, chomp: true)
    super
  end
end

"line1\nline2\n".lines
# => ["line1\n", "line2\n"]

String.prepend ChompingLines
String.ancestors
# => [ChompingLines, String, ...]

"line1\nline2\n".lines
# => ["line1", "line2"]
    

Here's an example of how we use this technique in one of Mynewsdesk's Rails applications to prevent cookies from being shared across different subdomains when the domain: :all option is used:

Ruby
# Since we need cookies to be shared across subdomains (www, publish etc)
# we've been in the habit of using the domain: :all option for Rails session
# and other cookies. This makes Rails set cookies at the root domain which
# allows them to be shared across all subdomains.
#
# The issue with this approach arises when mounting the Rails app on different
# subdomains as we do on mnd-review.com, e.g. pull-request-1.mnd-review.com and
# pull-request-2.mnd-review.com. Since cookies are set on .mnd-review.com, they end up
# shared across different apps, wreaking havoc in the process.
#
# To mitigate this issue, this monkey patch intercepts any cookies set with the
# "all" option and sets them on HTTP_DOMAIN instead. It also checks if
# request.host matches HTTP_DOMAIN to avoid messing up hosted newsrooms.

module DefaultCookieDomain
  # The handle_options method gets called by all methods that set and delete cookies
  def handle_options(options)
    if options[:domain].to_s == "all" && request.host.include?(HTTP_DOMAIN)
      options[:domain] = HTTP_DOMAIN
    end

    super
  end
end

ActionDispatch::Cookies::CookieJar.prepend(DefaultCookieDomain)
    

Note of caution: In these kinds of circumstances, when we are not in control of the code we're modifying, it's important to have tests covering the behaviour of the code we're changing to avoid accidentally breaking something.

History

Module#prepend was added in Ruby 2.0, in 2013, and has since become the idiomatic way to handle sensitive monkey patching in Ruby.

Before that, when we needed to modify the behaviour of an existing method and still wanted to be able to call the original method, we used to alias the original method before redefining it:

Ruby
class String
  alias_method :original_lines, :lines

  def lines(record_separator = $/, chomp: true)
    original_lines(record_separator, chomp: chomp)
  end
end
    
Reason #94 ?