非標準な Headers.prototype.getAll を通じて見る JavaScript ランタイムの違い
Headers
について
Headers
は WHATWG の Fetch Living Standard で定義されている HTTP リクエスト/レスポンスヘッダーを扱う JavaScript のクラスです。
const headers = new Headers();
headers.append("Content-Type", "application/json");
console.log(headers.get("Content-Type")); // => "application/json"
重複したヘッダーは結合される
RFC9110 HTTP Semantics にて重複したヘッダー(フィールド)が送られてきた場合はカンマ区切りで結合するように定義されています。
When a field name is only present once in a section, the combined "field value" for that field consists of the corresponding field line value. When a field name is repeated within a section, its combined field value consists of the list of corresponding field line values within that section, concatenated in order, with each field line value separated by a comma.
For example, this section:
Example-Field: Foo, Bar
Example-Field: Bazcontains two field lines, both with the field name "Example-Field". The first field line has a field line value of "Foo, Bar", while the second field line value is "Baz". The field value for "Example-Field" is the list "Foo, Bar, Baz".
この仕様に倣って Headers.prototype.get
を使った場合 ", "
で結合するようになっています。
const headers = new Headers();
headers.append("Example-Field", "Foo, Bar");
headers.append("Example-Field", "Baz");
console.log(headers.get("Example-Field")); // => "Foo, Bar, Baz"
重複したヘッダーが結合されると困るケース
基本的に重複したヘッダーが結合されても特に困ることはありません。しかし一つだけ例外があります。それは Set-Cookie
ヘッダーです。
Set-Cookie
ヘッダーはブラウザにクッキーを設定する際にサーバーから送るレスポンスヘッダーです。一度のレスポンスで複数のクッキーを追加するには複数の Set-Cookie
ヘッダーを必要とします。 また Set-Cookie
ヘッダーの特徴として値にカンマを含んでいる可能性があります。
Set-Cookie: a=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT
Set-Cookie: b=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT
サーバーサイド JavaScript を使ってプロキシサーバーを建てることになったとしましょう。fetch
を使って他のサーバーから得た Set-Cookie
ヘッダーの情報を取得したいとします。
Set-Cookie
ヘッダーの情報を得るために Headers.prototype.get
を使うと結合された文字列が取得されてしまいます。値にカンマを含む可能性があることから単に String.prototype.split
を使うわけにもいきません。そのためだけにパーサーを用意するのも手間です。
さてどうしたらいいでしょうか。
Fetch Living Standard での議論
Set-Cookie
ヘッダーの問題を解決するために Fetch Living Standard に Headers.prototype.getAll
メソッドを追加する[1]イシューがたっています。
議論が進み、どちらかといえば Set-Cookie
ヘッダーを特別扱いし、Headers.prototype.getSetCookie
メソッドを追加する方向で話が進んでいます。
各ランタイムの対応
各ランタイムで対応が大きく異なっています。
Deno
Deno は Web 標準を大事にしており、標準にないものを直接的にランタイムに追加しません[2]。
Deno Land inc. に所属している lucacasonato さんが Fetch Living Standard に Headers.prototype.getSetCookie
を追加する PR を作っています。これが取り込まれたら Deno 側にも反映されることかと思われます。
Node.js
さきほどの PR 内のコメントにて Node.js 開発メンバーの mcollina さんが Web 標準の仕様が取り込まれたら実装すると発言しています。
workerd (Cloudflare Workers)
Cloudflare Workers のテックリーダーである kentonv さんが2019年12月に Set-Cookie
の対応が必要なため、Web 標準にはないものの Headers.prototype.getAll
を独自実装する決断をしたというコメントを残しています。
OSS として公開された2022年9月のタイミングで既に実装されていたことがわかります。Set-Cookie
以外に対して使おうとすると例外を投げるようになっています。
一方で Web 標準に可能な限り則るというスタンスではあるため、Headers.prototype.getSetCookie
が標準に取り込まれる際に実装する準備がなされています。
edge-runtime (Vercel)
こちらも OSS として公開された2022年6月のタイミングで既に Headers.prototype.getAll
が独自実装されていたのがわかります。同様に Set-Cookie
以外に対して使おうとすると例外を投げます。
Bun
Bun は Web 標準にない拡張を便利だからという理由でカジュアルに取り込んでいっています。
議論の結論を待たずに Headers.prototype.getAll
と Headers.prototype.getSetCookie
の両方とも独自実装されています。
おわりに
今回は Headers.prototype.getAll
を例にして各ランタイムを比較してみました。
他にも Node.js の非標準な AsyncLocalStorage
に対して各ランタイムでスタンスが異なっていたりと面白いです。みなさんも JavaScript ランタイムの動向をウォッチするのはいかがでしょうか。
Discussion