とある Ruby のプロジェクトで依存 gem のバージョンを上げたらエラーが出たのでその原因を探った。
起こったこと
ある種のウェブアプリ機能を持った gem を開発し,長らく多数のプロジェクトで使っていた。
この gem は rack という gem を利用している1。rack への依存はとくにバージョンを指定していない。
ある日,bundle update
で gem たちのバージョンを上げたところ,rack もバージョン 2.2.4 から 3.0.0 に上がった。
メジャーバージョンが上がったので何かしら非互換性があるかとは予想したが,起動しようとしたら案の定
cannot load such file -- rack/server (LoadError)
が出て死んだ。
rack/server って何だっけ?
最小再現コード
今回発生したのと同じ問題を最小再現コードで示すと以下のようになる。
まず Gemfile。rack と webrick のみを使う。webrick はウェブサーバー。
source "https://rubygems.org"
gem "rack"
gem "webrick"
次に,ウェブアプリ。現在時刻を text/plain(つまり HTML じゃなくてただのテキスト)で返すだけの簡単なもの。
require "bundler"
Bundler.require
class App
def call(env) # 引数は使ってない
# レスポンスボディー(配列の形にする)
body = ["Now #{ Time.now }"]
# ステータス,ヘッダー,ボディーの三つ組を返す
[200, {"Conent-Type" => "text/plain"}, body]
end
end
Rack::Server.new(app: App.new).start
最終行で Rack::Server
を動かしている。
Rack::Server
を生成するとき,app
として「call
に反応するオブジェクト」を渡してやる。
この単純なスクリプトを動かし,ブラウザーで http://localhost:8080 にアクセスすると,画面に
Now 2022-09-17 18:00:57 +0900
みたいなものが表示される。
ところが,2022 年 9 月 6 日に rack 3.0.0 がリリースされ,このバージョンを使うと
Rack::Server.new(app: App.new).start
のところで件の LoadError が出るようになった。
原因と対策
rack のアップデートに伴う現象なので,もちろん
gem "rack", "< 3"
のようにバージョンを 3 未満に抑えてやればいちおう解決するわけだが,ここはきちんと原因を調べて最新の rack で動くようにしておきたい。
rack の CHANGELOG を見に行くと,[3.0.0.beta1] - 2022-08-08 の「Removed」のところに,「Rack::Server
等を別 gem に分離した」というようなことが書いてある。
その別 gem というのは,調べたところ rackup であるらしい(CHANGELOG に具体的な gem 名も書いておいてほしかった)。
ではこれを使おう。
rackup gem を使う
まずは Gemfile を以下のように変更してみた。
source "https://rubygems.org"
gem "rackup"
gem "webrick"
rackup は rack に依存しているので Gemfile に rack を書く必要はないはず。
これで bundle install
する。
rack gem はバージョン 3 未満で rackup
というコマンドを持っていた。rackup gem も同名のコマンドを持っているので,こいつをシステムにインストールするとコマンドを上書きしてしまう。一抹の不安がよぎるが,まあ今後,全プロジェクトで rack 2 系から 3 系に移行していくので,もし問題が生じてもどうにかできるかな。
さて,この状態で件のスクリプトを動かすと,今度は
uninitialized constant Rack::Server (NameError)
が出た。
おぅ! rackup gem には Rack::Server が無いんかいっ。
調べたところ,Rack::Server
を単純に Rackup::Server
に書き換えればよいことが分かった。
予想よりずっと簡単な修正で解決した。めでたしめでたし。
修正後のスクリプトも載せておく:
require "bundler"
Bundler.require
class App
def call(env)
body = ["Now #{ Time.now }"]
[200, {"Conent-Type" => "text/plain"}, body]
end
end
Rackup::Server.new(app: App.new).start
追記
Gemfile に rackup と rack を両方書いた状態で Rack::Server
を使うと NameError が出ずにアプリが動くが,
Rack::Server is deprecated and replaced by Rackup::Server
と警告が出る。
まとめ
今まで Rack::Server
を使っていたプロジェクトは,これからは rackup gem の Rackup::Server
を使うべし。
-
rack を一言で言えばウェブアプリとウェブサーバーの仲立ちをするライブラリー。ウェブサーバーの差異を吸収する働きがあり,ウェブサーバーの取り替えが簡単になる。 ↩