Help us understand the problem. What is going on with this article?

sinatraのバージョンあげたらIndifferentHashで逝った話

More than 1 year has passed since last update.

この記事はLIFULL Advent Calendarその2の記事です。
空いてたのでこないだみつけたsinatra関係の小ネタ投下しときます。

概要

最近とあるプロジェクトで使ってたとっても古いsinatraのバージョンを上げたら思わぬところでおもしろバグが生まれてたので紹介
検証したsinatraのバージョンは2.0.4

再現コード

なんかパラメータを受け取って、そのキーに対応する変換処理を値にかけるみたいな処理があったとします。

app.rb
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

IndifferentHash

といった感じで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として外から与えるテスト書きがちなので結構見落とされやすくて怖いなと思う次第。

以上、深夜にみつけた少し笑える現象でした。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away