この記事はLIFULL Advent Calendarその2の記事です。
空いてたのでこないだみつけたsinatra関係の小ネタ投下しときます。
概要
最近とあるプロジェクトで使ってたとっても古いsinatraのバージョンを上げたら思わぬところでおもしろバグが生まれてたので紹介
検証したsinatraのバージョンは2.0.4
再現コード
なんかパラメータを受け取って、そのキーに対応する変換処理を値にかけるみたいな処理があったとします。
require 'sinatra/base'
require 'haml'
require 'active_support/all'
require_relative 'models/init'
class Server < Sinatra::Base
get '/' do
converter = {
hoge: proc{|v| v.to_i + 1}
}
params.symbolize_keys!
converted = params.keys.each_with_object({}) do |key, dt|
dt[key] = converter[key].call(params[key])
end
haml :index, locals: {converted: converted}
end
end
そしてサーバー起動して http://{host}/?hoge=123
にアクセスします。
コードの意図はおいといてconvertedには{hoge: 124}
がはいりそうなものですが、このコードは__converterにそんなキーないよ__ って旨のエラーになります。
paramsには{"hoge"=>123}
が渡ってきてるので、それをsymbolizeして{hoge: 123}
にしてると流れ的によめるので問題なさそうに見えますね
実際、params[:hoge]
とアクセスすると123
を得ることができます
原因
どうみても正常な振る舞いにしか見えません。
となると考えられる可能性は___こいつは実はHashではない___説です。
恐る恐るparams.class
してみると
puts params.class
# Sinatra::IndifferentHash
Sinatra::IndifferenceHash ???
どうやらsinatraはどこかのバージョンアップのタイミングでparamsを普通のHashじゃなくてSinatra::IndifferentHashというものにかえてるみたいです。
で、これは実装みてみると...
module Sinatra
class IndifferentHash < Hash
...
def []=(key, value)
super(convert_key(key), convert_value(value))
end
...
def convert_key(key)
key.is_a?(Symbol) ? key.to_s : key
end
end
end
といった感じでHashを継承しているものの、[]=
を上書きして代入時にキーがsymbolだったら文字列に戻してからセットするようになっています。
今回のsymbolize_keys!は破壊的メソッドなのでactive support内部でself(=params)に対してself[key.to_sym] = delete(val)
みたいなことをしています。
activesupport symbolize_keys!
keys.each do |key|
self[yield(key)] = delete(key)
end
しかしこのself[key.to_sym] = xxx
がIndifferentHashの[]=
の中で無情にも元の文字列に戻されてるのです。
実に無意味...
見つけにくい
IndifferentHashはアクセサーもキーがsymbolだったらstringになおすようになってるので外からの観測でみつけるのは難しいですが、今回のようにいったんparams.keysと取り出してしまえばsymbolize_keys!したはずなのに文字列型のキーが羅列されるのでそれをもとにした処理を書くと死につながるので油断できないですね。
rspec側もparamsをHashとして外から与えるテスト書きがちなので結構見落とされやすくて怖いなと思う次第。
以上、深夜にみつけた少し笑える現象でした。