🐼

JavaScriptのnew URL()には気をつけろ

2023/09/11に公開

概要

  • new URL("") で独自のスキーマを扱う際には要注意
    • Chrome, Firefox, Edgeあたりは他と異なる挙動を示すぞ!
  • 実装の違う何かでPlaygroundするような愚を犯すべからず

背景

URLPatternなんてイカしたものを見つけてしまった。
URLのマッチングと、パス引数の解析をやってくれるらしい。
おいおいその辺のFWがあっちこっちで再開発していたアレが、Web APIの標準実装に載るのか!?
どこでも簡易ルーター作れて捗るんじゃないのォ!?とDenoでいじってみた。

某Vimプラグイン用に独自のスキーマを定義し、その処理をパスに従ってさばくルーターを書いてみようと思ったのだ。
ここでは皆さんの馴染みのある(?)スキーマとして、 git:// を使ったとして読んでもらおう。

過ち

何をどうひっくり返したらそういうことが思いつくのか自分でも良くわからん。
初手にChromeの開発者ツールで URLPattern がどういう動きを見せるのか確認した。

console.log(new URLPattern({protocol:"git", hostname: "foo"}).exec("git://foo"));
null

一見するとマッチしそうだが、これ、どうやらnullを返してくる。直観に反している。
そこでnew URL("git://foo")としてみると何が返ってくるのか確認してみた。

console.log(new URL("git://foo"));
{protocol: "git:", pathname: "//foo", ...}

なんとまあ。

hostnameではなくpathnameに、それも//込みという変わった区切り方で入ってくる。
よくわからんが、https://などの特別なスキーマの時だけは://の前後でプロトコルとホストに切り分けているようだ。

console.log(new URL("https://foo"));
{protocol: "https:", host: "foo", hostname: "foo", ...}

URLというものの扱いを考えると納得感は薄いが、Webのためのものだしな、と諦めてURLPatternも次のように使うことにした。

console.log(new URLPattern({protocol:"git:", pathname: "//foo"}).exec("git://foo"))
{protocol: {input: "git", ...}, pathname: {input: "foo", ...}}

発見

しかし、これでいざ実装してみると、まともに動かないのである。
Denoで実装した簡易ルーターはこのマッチでnullを返す。
何じゃラホイ、と思ってイジってみると、最初に直観で記述したマッチの方が使えるらしい。

console.log(new URLPattern({protocol:"git", hostname: "foo"}).exec("git://foo"));
{ protocol: { input: "git", ... }, hostname: { input: "foo", ... }, ... }

URLPatternはまだMDNでもExperimentalのマークが付けられてる状態だ。
Denoはまだ歴史が浅いことを考えるとDenoのバグだろう、とDenoの有識者に確認してみることにした…のだが、ここでそもそも今回の仕様の差異に気づく。

console.log(new URL("git://foo"))

  • Chrome上で実行した結果:{protocol: "git:", pathname: "//foo", ...}
  • Denoで実行した結果:{protocol: "git:", host: "foo", hostname: "foo", ...}

URLという超基本的なクラスの挙動がズレているのである!

結局どういうことなのか

私のWeb APIやJavaScript実装への理解では、とても追いつけなかったのだが、どうやらChromeやFirefoxの実装は、他とはズレているようなのだ。

Deno-JP Slackでとても親切に調べてくださった方によると、
Web Plaform TestsというところでChromeやEdge、Firefoxの挙動はテスト結果でNGとされている。

WPT の実行結果を見た感じ Safari は Firefox や Chrome と違って host に入るようになっているみたいです。
https://wpt.fyi/results/url/url-constructor.any.html%3Fexclude%3D(file|javascript|mailto)?label=experimental&label=master&aligned

実際、console.log(new URL("git://foo"))を各環境で実装してみると…

  • Chrome上で実行した結果:{protocol: "git:", pathname: "//foo", ...}
  • Firefox上で実行した結果:{protocol: "git:", pathname: "//foo", ...}
  • Safariで実行した結果:{protocol: "git:", host: "foo", hostname: "foo", ...}
  • Nodeで実行した結果:{protocol: "git:", host: "foo", hostname: "foo", ...}
  • Denoで実行した結果:{protocol: "git:", host: "foo", hostname: "foo", ...}

とまあ、Chromeや(EdgeはChromeと同じエンジンなので結果が同じなのは当然として)Firefoxの方がマイノリティらしい。

結論

正直、Web Platform Testsの立ち位置が私にはよく分からないので、 誰が悪いのか は判然としない。

しかし、ブラウザ上で扱うURLはほとんどがhttps://などの特別なスキーマに限られるため、両ブラウザでも問題にならないのだろう。

また、この手のブラウザ間の挙動の差異はPolyfillが作られている事が多い。
おそらく多くの開発者にとってはBrowserifyなどを通すことでPolyfillによる恩恵を受けて問題となっていないのではないか。(未調査)

教訓

大切なことは自分が利用しようと思っている実装系以外をPlaygroundとして使わない事であろうと思う。
ちょっとスクリプトを書いてdeno runを呼べば良いだけのことを、Chromeの開発者ツールなんぞで試した自分が悪い。

実装系によって挙動が違うものはたくさんあるので、JavaScriptに限らず「試すなら素直に同じ実装系を」は胸に刻んでおきたい。

Discussion