URLSearchParams は使って良い

概要

  • URLSearchParams の仕様は HTML form submission の仕様と揃えられている。
  • これを RFC 3986 違反と言うのはやめてほしい。

本文

この記事では("URLSearchParams RFC" で検索して出てきがちな)以下のような主張から URLSearchParams を弁護していきます。

URLSearchParams を使ったら *%2A に変換せずそのままにしていた。RFC 3986 で * は予約された文字なので厳密には仕様に準拠していない。

実は WHATWG の出している URL Standard

Align RFC 3986 and RFC 3987 with contemporary implementations and obsolete the RFCs in the process.

と書かれているので、もし仕様変更があったとしても WHATWG が正統とみなすというので終わる話だったのですが、中身を読んでしまったので確認した話が続きます。


URLSearchParams の仕様 https://url.spec.whatwg.org/#urlsearchparams の Note は URL.searchURL.searchParams の差の説明を目的としているようにも読めるが、ともかく application/x-www-form-urlencoded を利用することが書かれている。名前のとおり HTML form submission で使われる形式で、エンコード対象は

https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set

The application/x-www-form-urlencoded percent-encode set contains all code points, except the ASCII alphanumeric, U+002A (*), U+002D (-), U+002E (.), and U+005F (_).

なので RFC 3986 で unreserved の記号 -._~ と差がある。


そもそも RFC 3986 の reserved character とは何なのか。

https://datatracker.ietf.org/doc/html/rfc3986#section-2.2

reserved    = gen-delims / sub-delims

gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
            / "*" / "+" / "," / ";" / "="

URL をコンポーネントに分解するのに用いる区切り文字 (gen-delims) より多くを予約しているらしい。

The purpose of reserved characters is to provide a set of delimiting characters that are distinguishable from other data within a URI.

で始まる段落を読むと、目的は区切り文字として使用可能にすること、実装は percent encode するしないを区別することだと分かる。より具体的に言えば、

  • sub-delims が &, = を含むので以下を区別することができる:
  > new URLSearchParams([['x', 'a'], ['y', 'b&y=c']]).toString()
  'x=a&y=b%26y%3Dc'
  > new URLSearchParams([['x', 'a&y=b'], ['y', 'c']]).toString()
  'x=a%26y%3Db&y=c'
  • sub-delims が + を含むことが form data のスペースを +エンコードする実装を許している一方、そのために form data の +%2Bエンコードすることと直接の関係はない。

すなわち reserved characters は勝手に encode, decode されないという仕様であって、一律に使うなと言われているわけではない。実際、RFC 3986 が query について定める範囲では

pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
query       = *( pchar / "/" / "?" )

なので #[] 以外の reserved characters は使用が想定されている。

微妙なところとしては "specifically allowed by the URI scheme to represent data in that component" でなければ送信は厳格に受信は寛容にと(いつものパターンで)書かれている。こうなると RFC 3986 に閉じた話でなくなり、現実は WHATWG に反映されているはずなのでそちらを見ると、


ちなみに URL に現れない printable ASCII は"<>\^`{|} の 9 文字。これに加え

  • # fragment 開始専用
  • % percent encoding 専用
  • [] IPv6 (or later) を囲む専用

が除外されていることが古い仕様書では説明されている。https://datatracker.ietf.org/doc/html/rfc2396#section-2.4.3

最近の仕様書には使える文字のほうのリストしかないようだ。https://url.spec.whatwg.org/#url-code-points


また、 encodeURIComponent についてはどの scheme のどの component に使うか分からないから reserved character すべてをエンコードする仕様だったほうが適切という主張に理がある。(sub-delims の差なので「URL の」パース結果が変わるようなひどいことにはならないが。)

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986

URL の query 部分に限って言えば form data から * がそのまま入る仕様を消すとしてもさらに十分な時間が経つまでは * に別の意味を持たせる状況は考えにくいので URLSearchParams が将来の標準に沿っているかの点でも懸念は少ないだろう。