POSTするデータを、(プレビューのために)javascriptでGETアクセスするような処理を書いていてハマった話。
発端は、textareaに'(シングルクオートまたはアポストロフィー)が入ると、Railsがそこから先のパラメーターを無視しちゃうっていうこと。いろいろ調べた結果、以下のことがわかった(Railsのバージョンは2.0.2)。
URIを定義する2つのRFC
URIの構文はRFCで定義されている。これには2つあって、従来のRFC2396(1998年発行)と、RFC3986(2005年発行)だ。
RFC3986によれば、
This document obsoletes [RFC2396], which merged "Uniform Resource Locators" [RFC1738] and "Relative Uniform Resource Locators" [RFC1808] in order to define a single, generic syntax for all URIs.
ということなので、RFC3986が分散したURIの定義を全て統合することを狙っている様子。2008年にリリースされたRails2.0は、RFC3986にしたがってURIのパラメーターをパースするようだ(解析部分を確かめたわけではないんだけど、"3986"でgrepするとテストの部分が何箇所かヒットするので、意識しているのは間違いない)。
で、この2つには、URIに使ってよい文字のリストに違いがある。
URIに使ってよい文字の違い
まずは、RFC2396から、BNFによる定義の一部を抜粋してみる。
RFC2396 Appendix A. Collected BNF for URI (抜粋)
URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
absoluteURI = scheme ":" ( hier_part | opaque_part )
hier_part = ( net_path | abs_path ) [ "?" query ]
query = *uric
uric = reserved | unreserved | escaped
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
"$" | ","
unreserved = alphanum | mark
mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
"(" | ")"つまり、markに挙げられている文字はunreserved(非予約語)で、これは普通に使ってよいことになっている。現に、javascriptのencodeURIComponentでは、unreservedな文字は変換しない。
一方そのころRFC3986では、reservedとunreservedな文字のリストに変化がある。
RFC3986 Appendix A. Collected ABNF for URI
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
query = *( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="sub-delimsはreservedであるから、unreservedとされている記号はハイフン・ドット・アンダースコア・チルダだけだ。
つまり、「!'()*」の五文字が、以前はunreservedであったが現在はreservedになった、ということ。
encodeURIComponentはRFC2396なので
ところで、Javascript(ECMAScript)のencodeURIComponent関数はRFC2396に従ったエンコード処理をするので、「!'()*-._~」の記号はエンコードしない。だから、RFC3986準拠のURIを生成するには、encodeURIComponentの処理の上で、さらに「!'()*」は自前でエンコードしなくちゃいけない。
自前の関数を作らねば
というわけで、たいした処理ではないけど、自前関数。
function encodeURIComponentRFC3986(str) { return encodeURIComponent(str). replace(/[!*'()]/g, function(p){ return "%" + p.charCodeAt(0).toString(16); }); }
まぁ、textareaをGETで送るっていう変則的な処理の場合だから必要になるケースは少ないけど、頭に入れておくと役に立つかも。