URIに使ってよい文字の話 - RFC2396 と RFC3986
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で送るっていう変則的な処理の場合だから必要になるケースは少ないけど、頭に入れておくと役に立つかも。