Ruby 2.7.0に上げてFrozenErrorを踏んだ
最初に結論ですが自分のミスなので言語やライブラリの問題ではないです。
自分のサービスを全部2.6.5から2.7.0にあげていったときにFrozenErrorを踏んでしまったのですが、 該当箇所のコードは以下のようなものでした。
endpoint = 'https://hoge.com/api/v1/hoge' resp = HTTP.get(hoge) JSON.parse(resp.body.to_s)
最後の JSON.parse
で FrozenError can't modify frozen String: ""
が出ます。
Railsのlibとして配置した自作httpクライアントで、特にrequireなしで使っていました。
他の箇所は Net::HTTP
で通信していたのでそちらで書き直すとちゃんと動いたのですが、
HTTP
って標準ライブラリじゃないんだっけ?と思って調べてみると普通にgemでした。
refs: GitHub - httprb/http: HTTP (The Gem! a.k.a. http.rb) - a fast Ruby HTTP client with a chainable API, streaming support, and timeouts
しかも他のライブラリに依存して入っていたものを無意識に使っていたせいで、バージョンが低く2.7に未対応のバージョンを使っていました。 requireも書かずに副作用でコードを書いてしまっていて、かなり微妙な理由で例外を出してしまっていた。。
ちなみにライブラリ側では以下のようなコードの部分で例外が出ていました。
@contents = String.new("").force_encoding(@encoding) while (chunk = @stream.readpartial) @contents << chunk.force_encoding(@encoding) # raise chunk.clear # deallocate string end
@contents
は String.new("")
で定義されているので一見 FrozenError
の原因にならなさそうですが、
raiseしているのは @contents
への追加部分ではなく chunk
に対する #force_encoding
です。
この問題はちょうど以下のコミットで直されていました。
- chunk.to_s + chunk || "".b
String#b
を使うことでメソッドが返す文字列 == 変更可能として返すという副作用に相乗りしている模様。
refs: instance method String#b (Ruby 2.7.0 リファレンスマニュアル)
今回のケースは恐らくライブラリの後続処理で文字列操作を行わないのでパフォーマンス影響はないという判断なのでしょうか。 もしライブラリの中で文字列を再度扱うようなケースがある場合はここもちゃんとfrozenしておくほうが良さそうな気はします。
初心者のようなミスをしてしまいましたが、今後踏みそうなやつを早めに踏んでおけてよかったという気持ちです。