本記事は、はてなエンジニア Advent Calendar 2020 の 18 日目の記事です。昨日は id:YaaMaa さんでした。
社内チャットではこの話で盛り上がったときにトライ木も作られており、良い頭の体操になっていました。
さて、本題。
Hatena::Let を眺めていて、こんな URL に気づいた。
http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=
クエリパラメータにセミコロン……!
パッと考えるとこれは
{ code_id => "g5G0uOeEqfcA;key=" }
となりそうで、というか Ruby で実際にパースするとそうなる。
uri = URI("http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=") URI.decode_www_form(uri.query) #=> [["code_id", "g5G0uOeEqfcA;key="]]
「こんなカッコ良くもない URL で独自規格作らないでよ」が最初の感想だったんだけど、ソースコードを読むと普通に $r->req->param('code_id')
や $r->req->param('key')
で取得している。
param
の実装である Plack::Request::_query_parameters
を見に行くと
sub _query_parameters { my $self = shift; $self->env->{'plack.request.query_parameters'} ||= parse_urlencoded_arrayref($self->env->{'QUERY_STRING'}); }
となっていて、WWW::Form::UrlEncoded::parse_urlencoded_arrayref
がパーサ。パーサの実装は
if ( src[i] == '&' || src[i] == ';') {
https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/lib/WWW/Form/UrlEncoded/XS.xs#L246
と &
と ;
の両方をクエリパラメータのデリミタとしているし、テストコードにも含まれている!
'a=b&c=d' => ["a","b","c","d"] 'a=b;c=d' => ["a","b","c","d"] 'a=1&b=2;c=3' => ["a","1","b","2","c","3"] 'a==b&c==d' => ["a","=b","c","=d"] 'a=b& c=d' => ["a","b","c","d"] 'a=b; c=d' => ["a","b","c","d"] 'a=b; c =d' => ["a","b","c ","d"] 'a=b;c= d ' => ["a","b","c"," d "] 'a=b&+c=d' => ["a","b"," c","d"] 'a=b&+c+=d' => ["a","b"," c ","d"] 'a=b&c=+d+' => ["a","b","c"," d "] 'a=b&%20c=d' => ["a","b"," c","d"] 'a=b&%20c%20=d' => ["a","b"," c ","d"] 'a=b&c=%20d%20' => ["a","b","c"," d "] 'a&c=d' => ["a","","c","d"] 'a=b&=d' => ["a","b","","d"] 'a=b&=' => ["a","b","",""] 'a=&' => ["a","","",""] '&' => ["","","",""] '=' => ["",""] '' => []
https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/t/01_parse.t#L26
ふえぇぇって思ってググると QUERY_STRING 中のパラメータの区切りは必ずしも '&' ではない - 理系学生日記 に行き着いて、
We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.
https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2
という記述を見つけた。
えーーじゃあ Ruby でさっきダメだったのは何!!!って話になったのでまず Rack を見に行ったら id:Nyoho さんが 2015 年に PR 出していて、W3C の同じ記述を参照しながら
Fix to use semicolons as separators for GET not for POST.
と GET リクエストのときのパース時のみ ;
でも分割できるように修正している。当時 tDiary にも Issue が上がっていて、
http://www.tamoot.net/d/?year=2015;month=1Q;category=Ruby
という URL がパースできなくなったトノコト。なるほど、tDiary にもセミコロン区切りがあったのね。CGI 時代の歴史的経緯っぽい香りがする。
閑話休題。「Rack は通すんじゃん」って思いつつ URI.decode_www_form
の実装を見に行くと
# This refers http://url.spec.whatwg.org/#concept-urlencoded-parser, # so this supports only &-separator, and doesn't support ;-separator.
https://github.com/ruby/ruby/blob/v2_7_2/lib/uri/common.rb#L443-L444
とわざわざ「;
は対応しないよ」ってコメントが書いてある。
更に blame すると
lib/uri/common.rb (URI.decode_www_form): follow current URL Standard.
- # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data + # This refers http://url.spec.whatwg.org/#concept-urlencoded-parser , + # so this supports only &-separator, don't support ;-separator.
https://github.com/ruby/ruby/commit/4a50d44
とのことで、 WHATWG の application/x-www-form-urlencoded
の仕様に則ったらしい。
http://url.spec.whatwg.org/#concept-urlencoded-parser
こちらでは &
のみがデリミタと定義されている。
というわけで、application/x-www-form-urlencoded
で POST したときは仕様があるけど、URL では query string のパース方法までは決まっていないんじゃないかなぁ。*1
;
を使っても良いと解釈できずに URL Escape してしまうクローラがいる等の弊害もあり、今となっては敢えて使う必要はなさそう *2 なのでただの雑学ですが、最近見た 10 年モノの面白コードの話でした。
はてなエンジニア Advent Calendar 2020、明日 19 日目は id:Pasta-K さんです。