【RubyKaigi 2026レポート】 Ruby::Boxで考える、Net::HTTPへのモンキーパッチの閉じ込め方

精算管理システムの開発を担当しているjuriです。

2026年4月22日〜24日に函館で開催された、RubyKaigi 2026に参加してきました。

函館行きの飛行機の欠航がいくつか出るなど嵐を呼ぶ幕開けでしたが(東北・北海道新幹線「はやぶさ」の存在に感謝です)、 嵐を吹き飛ばすような勢いの熱いセッションの数々とスポンサーさんのブースの熱狂により、大変充実した3日間となりました!

特に初日のキーノートThe Journey of Box Buildingで @tagomorisさんから発表のあった Ruby::Box に興味を持ち、「自社のコードベースで活かせる場面がありそうか」を考えながら触ってみたので、その記録を共有します。

Ruby::Boxとは

Ruby::Boxは、主にクラスやモジュールの定義を分離するための機能です。 Feature #21311として、Ruby4.0.0で導入されました。

Ruby4.0.0 リリースノート

キーノートで紹介されたユースケースは以下です。

  • モンキーパッチを当てるときに、想定したターゲットだけに変更を効かせたいとき
  • 同一プロセス内でBlue-Greenデプロイのような切り替え作業を行いたいとき
  • アプリのバージョン移行で、特定バージョンの影響範囲を閉じ込めたいとき

Ruby::Boxを動かす

Ruby4.0の実行環境を用意し、環境変数として RUBY_BOX=1 を設定します。

irb上で試しに動かしてみます。

irbを起動すると、Ruby::Boxは「experimental(実験的機能)」であるというwarningが出ます。 ※ オプションで、-W:no-experimentalと設定すると消えます

root@39761851b305:/app# irb
/usr/local/bin/ruby: warning: Ruby::Box is experimental, and the behavior may change in the future!
irb> box = Ruby::Box.new
irb> box.eval("class Hello; def self.greet = 'hi from box!'; end")
irb> box.eval("Hello.greet")   #=> "hi from box!"   ← box内では呼べる
irb> Hello.greet               #=> NameError: uninitialized constant Hello   ← root boxには Hello そのものが存在しない

このように、box内で定義したクラス(Hello)は box内に閉じ込められ、box外からは存在しないと言われます。

これまでのRubyでは、一度クラスを再定義するとプロセス全体に影響が及びましたが、Ruby::Boxは「箱(box)」ごとに独立した定数テーブルを持ちます。

これにより、同じ Net::HTTP という名前のクラスでも、box Aではデフォルト設定、box Bではカスタムパッチ適用済み、といった使い分けが可能になります。

利用例:Net::HTTPに対するモンキーパッチ

さて、ここからはモンキーパッチをしたいユースケースについて考えます。

どのアプリケーションでも外部通信との接続はつきものです。 例に漏れず私の開発しているアプリケーションでも、社内の別サービスとの接続やOAuth認証などいくつかの外部接続が発生します。

外部通信が長引くと該当のスレッドを占有してしまうため、スループット低下に繋がります。これを防ぐため、タイムアウト値をデフォルトの60秒から短めに設定したいケースは多いと思います。

Faraday(RubyのHTTPクライアントライブラリ)でインスタンス生成時に個別で設定をすることもできるのですが、内部的にFaradayを呼んでいるgemの場合、直接設定ができないケースがあります。 そのため、Net::HTTPに対してモンキーパッチをあてて、設定ができない(または設定が漏れている)場合でも狙ったタイムアウト値にするようにしていました。

config/initializers/default_timeout.rb

Rails.application.reloader.to_prepare do
  if defined?(Net::HTTP)
    module Net
      class HTTP
        prepend(Module.new do
          def initialize(*)
            super
            # override
            @open_timeout = 20
            @read_timeout = 20
            @write_timeout = 10
          end
        end)
      end
    end
  end
end

実践:Net::HTTPへの局所的なパッチ適用

全体一律でやるだけでなく、コンテキストによっては緩めたい (バッチ処理など)、あるいは厳しくしたい(管理画面など)ケースが出てきます。 Ruby::Boxならプロセス内に複数のポリシーを持たせることができるのではと考えました。

私の開発しているサービスだと、例えばこのように場合分けできます。

ユースケース 理想のタイムアウト値
社外ユーザからのリクエスト 短め
社内管理画面 中程度
バッチ処理 長め

まずはNet::HTTPに対するパッチを行ってみます。

require 'net/http'

# デフォルトのタイムアウト値
http = Net::HTTP.new('example.com', 80)

p http.open_timeout #=> 60
p http.read_timeout  #=> 60

box = Ruby::Box.new

# box内でパッチを適用
box.eval(<<~RUBY)
  require 'net/http'
  module Net
    class HTTP
      prepend(Module.new do
        def initialize(*)
          super
          @open_timeout = 5
          @read_timeout = 3
        end
      end)
    end
  end
  http = Net::HTTP.new('example.com', 80)
  p http.open_timeout #=> 5
  p http.read_timeout  #=> 3
RUBY

重要な点として、box内でのパッチ適用は、box外のNet::HTTPには影響を与えません。以下が確認コードです。

# box外でのデフォルト値(パッチ前)
http_before = Net::HTTP.new('example.com', 80)
p http_before.open_timeout #=> 60

box = Ruby::Box.new
box.eval(<<~RUBY)
  # ... box内でパッチ適用 ...
RUBY

# box外でのデフォルト値(パッチ後も変わらない)
http_after = Net::HTTP.new('example.com', 80)
p http_after.open_timeout #=> 60(box外は影響を受けていない)

このように、特定のboxに閉じ込められたパッチは、他のboxには一切影響を与えません。

続いて、「タイムアウトを指定する方法を持たないライブラリ」を想定して、ダミーファイル dummy_http_client.rb を作って検証します。 実際の業務でも、社内Gemで内部で Net::HTTP.new を直接呼んでいるが指定する方法がない、というケースは稀ではありません。

lib/dummy_http_client.rb

require 'net/http'

module DummyHttpClient
  def self.connection
    Net::HTTP.new('example.com', 80)
  end
end
timeout_box = Ruby::Box.new

timeout_box.eval(<<~RUBY)
  $LOAD_PATH.unshift '/app/experiments/lib'
  require 'dummy_http_client'
  module Net
    class HTTP
      prepend(Module.new do
        def initialize(*)
          super
          @open_timeout = 5
          @read_timeout = 3
        end
      end)
    end
  end
  http = DummyHttpClient.connection

  p http.open_timeout #=> 5
  p http.read_timeout  #=> 3
RUBY

dummy_http_client が内部で Net::HTTP.new を呼び出していますが、box内のモンキーパッチが自動的に適用されていることがわかります。一度requireされてロードされたgem内の処理であっても、box内の定義が適用されます。

今回のシンプルな検証の範囲内では、ライブラリに対するパッチも期待通り動作することを確認できました。

ただし、今回は initialize を上書きするにとどまっています。複雑な依存関係を持つ標準ライブラリに対して、どこまで副作用なくパッチを閉じ込められるかについては、さらなる検証が必要そうです。

また、実務レベルのgemへの適用についても、Bundler連携(#21806, #21798)やネイティブ拡張(#21809)といったRuby本体側の議論や課題解決を待つ必要がある部分も多そうです。

試してみて

私の開発しているサービスでは、いくつかモンキーパッチが利用されており、 Ruby::Box の恩恵を受けられる場面が大いにありそうです。 現状では experimental なので、プロダクションへの導入は今後の動向を観測しつつ検討したいと思います。

今回試してみて、紹介されているユースケース以外にも多くの可能性を秘めている機能だと思いました。 気になった方はぜひキーノートのスライドや公式ドキュメントを見てみてください!

あとがき

RubyKaigiの3日間でたくさん刺激を受け、ついに気になっていた Ruby::Box の機能を試すことができました。 @tagomorisさんのキーノートでも、「@shioyamaさんとの会話から Ruby::Box のアイデアが生まれた」とのことでしたが、 カンファレンスのいいところは、熱量を持った人たちとの会話や会話から生まれるアイデアとモチベーションにあると身をもって感じることができました!