http://example.com/path?k1=v1&k2=v2
においてk1, v1, k2, v2の部分に使える文字について調べてみた。
まとめ
-
*-._
の4文字と英数字はそのまま - スペースは
+
に置換する - それ以外はパーセントエンコードする
- JSの
encodeURIComponent
はこの4文字に加えて!'()~
もエスケープから除外している。encodeURIComponent
はRFC 2396準拠であり、古い。よってencodeURIComponent
が常に正かというとそういうわけではない。 - 現実には、スペースを
+
でなく%20
に置換してしまっても問題ない事が多い - それどころか、
*-._
の4文字もパーセントエンコーディングしてしまって問題ない事が多い
調査記録
- RFCを追っていくと、パーセントエンコードしなくていいのは
-._~
の4文字と解釈できそうだった。これはHTML5の仕様と微妙に違う。でもおそらくHTML5の仕様を信用した方がいいと思う。- クエリーパラメータの仕様を定めているのはRFC 1866 x-www-form-urlencodedである。
- RFC 1866は「スペースを
+
に変換し、URLで予約された文字をパーセントエンコードする」と定義している。 - RFC 3986によれば、unreservedと定義されているのは
-._~
の4文字。
- 現実には、スペースは
%20
、+
どちらになる場合もある。- x-www-form-urlencodedの仕様的には
+
の方が正しいのかもしれないが、現実的には%20
にした方が安全と思う。-
%20
なら受け取る側はまず間違いなくスペースと解釈するだろうから。 - curlも
--data-urlencode 'hoge= '
では%20
にする。
-
- JSの
encodeURIComponent
は%20
にするし、decodeURIComponent
は+
をスペースに置換しない。-
encodeURLComponent
が準拠するのはRFC 3986でなくRFC 2396である。つまり古い。RFC 1866は考慮していないと言える。
-
- x-www-form-urlencodedの仕様的には
- 関連RFCは
- RFC 2396 "Uniform Resource Identifiers (URI): Generic Syntax" (1998年発行)
- RFC 3986 "Uniform Resource Identifiers (URI): Generic Syntax" (2005年発行)
- 2396は「Obsoleted by: 3986」なので現在は3986だけを参照すればよい
- RFC 1866 "Hypertext Markup Language - 2.0" 8.2.1. The form-urlencoded Media Typeの部分
- encodeURI と decodeURI は URI 全体に適用するもので、encodeURIComponent と decodeURIComponent は URI の要素に適用するものと意図されている。
- Chromeのアドレスバーで「python ruby」と入れて検索するとURLは「python+ruby」になる
RFC 1866
8.2.1. form-urlencoded メディアタイプ(The form-urlencoded Media Type)
すべてのフォームのデフォルトエンコーディングは`application/x-www-form-urlencoded'です。このメディアタイプではフォームデータセットは以下のように表わされます。
1.フォームフィールドの名称と値は次のようにエスケープされます:**空白文字は'+'に置き換えられ、予約された文字記号は[URL]に従ってエスケープされます。すなわち、英数字以外は'%HH'――パーセント記号と記号のASCIIコードを16進数で表わしたもの――に置き換えられます。**複数行のテキストフィールドにおいて、改行はCRとLFの組み合わせ、すなわち'%0D%0A'で表わされます。
2.フィールドは文書に現われる順番に、'='により値と名称を区別し、'&'によりほかの組み合わせと区別して並べられます。値のないフィールドは無視されるでしょう。特に選択されていないラジオボタンやチェックボックスは符号化されたデータには現われませんが、VALUE属性を持つ隠しフィールドは現われます。
注意‐問い合わせフォーム提出からのURIは通常のハイパーリンクのアンカー形式として使用できます。不運なことに、'&'記号をフィールドの区切りに使用することは、SGML属性値において実体参照区切り子としてその記号を使うことと互いに影響を与えあいます。例えば、
http://host/?x=1&y=2
は、<a href="http://host/?x=1&y=2">
あるいは<a href="http://host/?x=1&y=2">
と書くべきです。HTTPサーバ、特にCGIはユーザーをトラブルから守るために、'&'の代わりに';'の使用をサポートすることを薦めます。
RFC 2396
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 = "-" | "_" | "." | "!" | "~" | "*" | "'" |
"(" | ")"
escaped = "%" hex hex
reserved(予約文字)はURIにおいて特殊な意味を持つ文字。よってこれらをパラメータで送りたい場合は?q=&
でなくq=%26
のようにURLエンコードしなければならない。
RFC 3986
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
RFC 2396にはあったreservedという文言がなくなっているが、sub-delims、":"、"@"がreservedに相当する。よって!'()*
の五文字が、以前はunreservedであったが現在はreservedになった、ということ。
JavaScriptのencodeURIComponent
RFC 2396の通り、9個の文字がエスケープされずに残っている。
%20
! ! equal
" %22
# %23
$ %24
% %25
& %26
' ' equal
( ( equal
) ) equal
* * equal
+ %2B
, %2C
- - equal
. . equal
/ %2F
: %3A
; %3B
< %3C
= %3D
> %3E
? %3F
@ %40
[ %5B
\ %5C
] %5D
^ %5E
_ _ equal
` %60
{ %7B
| %7C
} %7D
~ ~ equal
上記結果出力用のスクリプト
function encodeString(str) {
for (var j = 0; j < str.length; j++) {
var c = str[j];
console.log(c + "\t" + encodeURIComponent(c) + "\t" + (c === encodeURIComponent(c) ? "equal" : ""));
}
}
for (var i = 0x20; i < 0x7F; i++) {
var s = String.fromCharCode(i);
if (!s.match(/[A-Za-z0-9]/)) {
encodeString(s);
}
}
RubyのURI.encode_www_form_componentとencode_www_form
HTML5のw3cの仕様と同じく*-._
の4文字をそのまま残している。
+
! %21
" %22
# %23
$ %24
% %25
& %26
' %27
( %28
) %29
* * equal
+ %2B
, %2C
- - equal
. . equal
/ %2F
: %3A
; %3B
< %3C
= %3D
> %3E
? %3F
@ %40
[ %5B
\ %5C
] %5D
^ %5E
_ _ equal
` %60
{ %7B
| %7C
} %7D
~ %7E
require "uri"
(0x20...0x7F).each do |c|
s = c.chr
unless s =~ /[A-Za-z0-9]/
encoded = URI.encode_www_form_component(s)
puts "#{s}\t#{encoded}\t#{s == encoded ? 'equal' : ''}"
end
end