定食屋おろポン

おろしポン酢と青ネギはかけ放題です

Rubyのnet/httpで重複するキーをもつパラメータをPostする

net/http を使うと、単純にPostするのは至って簡単だ。

Net::HTTP.post_formメソッドを叩くだけで事足りる。

library net/http

require 'net/http'
require 'uri'

#例1: POSTするだけ
res = Net::HTTP.post_form(URI.parse('http://www.example.com/search'),
                          {'q'=>'ruby', 'max'=>'50'})
puts res.body

#例2: 認証付きで POST する
res = Net::HTTP.post_form(URI.parse('http://jack:pass@www.example.com/todo.cgi'),
                          {'from'=>'2005-01-01', 'to'=>'2005-03-31'})
puts res.body

#例3: より細かく制御する
url = URI.parse('http://www.example.com/todo.cgi')
req = Net::HTTP::Post.new(url.path)
req.basic_auth 'jack', 'pass'
req.set_form_data({'from'=>'2005-01-01', 'to'=>'2005-03-31'}, ';')
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
case res
when Net::HTTPSuccess, Net::HTTPRedirection
  # OK
else
  res.value
end

キーが重複するパラメータ問題

フォームで配列を送信するとしよう。

例えば、

somekey[] => "hoge",
somekey[] => "fuga",
somekey[] => "piyo",

といったパラメータだ。

post_formメソッドは、uriparamsの引数を取る。第一引数のuriURIで、第二引数のparamsHashだ。 つまり、こういう呼び出しになるだろう。

Net::HTTP.post_form(URI.parse('http://example.com'), {
  "somekey[]" => "hoge",
  "somekey[]" => "fuga",
  "somekey[]" => "piyo",
})

ここで問題が浮上する。Hashは重複するキーを持つことができない。上記の呼び出しは、このように変換されてしまう。

Net::HTTP.post_form(URI.parse('http://example.com'), {
  "somekey[]" => "piyo",
})

解決法

HTTPリクエストbodyにPostしたいデータを直接セットしてやることで回避した。 その際は、自分でPostするパラメータを文字列化し、URLエンコードする必要がある。具体的には、URI.encode_www_formを使う。

3行目を参照。

url = URI.parse('http://www.example.com/todo.cgi')
req = Net::HTTP::Post.new(url.path)
req.body = URI.encode_www_form [
  ["somekey[]", "hoge"], 
  ["somekey[]", "fuga"], 
  ["somekey[]", "piyo"]
]
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
case res
when Net::HTTPSuccess, Net::HTTPRedirection
  # OK
else
  res.value
end

キモは、URI.encode_www_formでURLエンコードする際に、配列の配列を渡すことだ。Hashじゃないからキーの重複も問題ない。

http://docs.ruby-lang.org/ja/2.1.0/class/URI.html#S_ENCODE_WWW_FORM

URI.encode_www_formメソッドの返り値はこうなる。

"somekey%5B%5D=hoge&somekey%5B%5D=fuga&somekey%5B%5D=piyo"

tips

Net::HTTP.post_formはHashを受け取ると書いたが、それはあくまでドキュメント上のはなしだ。 ソースを追えばわかるが、内部ではURI.encode_www_formを呼んでいるだけである。 (post_formの中でNet::HTTPHeader#set_form_dataが呼ばれ、その中でURI.encode_www_formしている。)

だから、post_formメソッドに(Hashではなく)配列の配列を渡しても動く。

Net::HTTP.post_form(URI.parse('http://example.com'), [
  ["somekey[]", "hoge"], 
  ["somekey[]", "fuga"], 
  ["somekey[]", "piyo"]
])

不安なら、set_debug_output($stderr)を使ってリクエストを覗いてみればいい。

ただundocumentedなので、こういう風に使っていいのかアヤシイ。