👀

非標準な Headers.prototype.getAll を通じて見る JavaScript ランタイムの違い

2023/01/25に公開約4,500字

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"

https://developer.mozilla.org/ja/docs/Web/API/Headers

重複したヘッダーは結合される

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: Baz

contains 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".

https://datatracker.ietf.org/doc/html/rfc9110#section-5.2

この仕様に倣って 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"

https://fetch.spec.whatwg.org/#concept-header-list-get

重複したヘッダーが結合されると困るケース

基本的に重複したヘッダーが結合されても特に困ることはありません。しかし一つだけ例外があります。それは Set-Cookie ヘッダーです。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/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]イシューがたっています。

https://github.com/whatwg/fetch/issues/973

議論が進み、どちらかといえば Set-Cookie ヘッダーを特別扱いし、Headers.prototype.getSetCookie メソッドを追加する方向で話が進んでいます。

各ランタイムの対応

各ランタイムで対応が大きく異なっています。

Deno

Deno は Web 標準を大事にしており、標準にないものを直接的にランタイムに追加しません[2]

Deno Land inc. に所属している lucacasonato さんが Fetch Living Standard に Headers.prototype.getSetCookie を追加する PR を作っています。これが取り込まれたら Deno 側にも反映されることかと思われます。

https://github.com/whatwg/fetch/pull/1346

Node.js

さきほどの PR 内のコメントにて Node.js 開発メンバーの mcollina さんが Web 標準の仕様が取り込まれたら実装すると発言しています。

https://github.com/whatwg/fetch/pull/1346#issuecomment-1171502837

workerd (Cloudflare Workers)

Cloudflare Workers のテックリーダーである kentonv さんが2019年12月に Set-Cookie の対応が必要なため、Web 標準にはないものの Headers.prototype.getAll を独自実装する決断をしたというコメントを残しています。

https://github.com/whatwg/fetch/issues/973#issuecomment-560136041

OSS として公開された2022年9月のタイミングで既に実装されていたことがわかります。Set-Cookie 以外に対して使おうとすると例外を投げるようになっています。

https://github.com/cloudflare/workerd/blob/a265d5c22bf2a6bc6394aa54db517ff86d6c788c/src/workerd/api/http.c%2B%2B#L229-L242

一方で Web 標準に可能な限り則るというスタンスではあるため、Headers.prototype.getSetCookie が標準に取り込まれる際に実装する準備がなされています。

https://github.com/cloudflare/workerd/pull/321

edge-runtime (Vercel)

こちらも OSS として公開された2022年6月のタイミングで既に Headers.prototype.getAll が独自実装されていたのがわかります。同様に Set-Cookie 以外に対して使おうとすると例外を投げます。

https://github.com/vercel/edge-runtime/blob/0b11a95e2f470d278db27982e4905febc6ac9bb7/packages/primitives/src/polyfills/undici.js#L125-L137

Bun

Bun は Web 標準にない拡張を便利だからという理由でカジュアルに取り込んでいっています。

議論の結論を待たずに Headers.prototype.getAllHeaders.prototype.getSetCookie の両方とも独自実装されています。

https://github.com/oven-sh/bun/commit/d84f79bcc16d4e748c8a9400ea1cdb03d7f963fb

https://bun.sh/blog/bun-v0.3.0#new-methods-on-headers

おわりに

今回は Headers.prototype.getAll を例にして各ランタイムを比較してみました。

他にも Node.js の非標準な AsyncLocalStorage に対して各ランタイムでスタンスが異なっていたりと面白いです。みなさんも JavaScript ランタイムの動向をウォッチするのはいかがでしょうか。

脚注
  1. 正確には「復活させる」が正しそうです。一度仕様に取り入れられたもののすぐ取り消されたようです。 ↩︎

  2. Web 標準でないものは Deno ネームスペースや deno_std モジュールで提供しています。 ↩︎

Discussion

ログインするとコメントできます