839の日記

趣味の話を書くブログです。

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.parseFrozenError 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

@contentsString.new("") で定義されているので一見 FrozenError の原因にならなさそうですが、 raiseしているのは @contents への追加部分ではなく chunk に対する #force_encoding です。
この問題はちょうど以下のコミットで直されていました。

-      chunk.to_s
+      chunk || "".b

refs: https://github.com/httprb/http/commit/8b1477a9fefc5879f7c864a38eabbfed92b7663d#diff-b31c2c9884d039f633a34d10a344d68b

String#b を使うことでメソッドが返す文字列 == 変更可能として返すという副作用に相乗りしている模様。 refs: instance method String#b (Ruby 2.7.0 リファレンスマニュアル)

今回のケースは恐らくライブラリの後続処理で文字列操作を行わないのでパフォーマンス影響はないという判断なのでしょうか。 もしライブラリの中で文字列を再度扱うようなケースがある場合はここもちゃんとfrozenしておくほうが良さそうな気はします。

初心者のようなミスをしてしまいましたが、今後踏みそうなやつを早めに踏んでおけてよかったという気持ちです。