【HTTP】URLクエリパラメータの些細な違いを無視してキャッシュしたいよねという話

`https://localhost/?a=1&b=2`と`https://localhost/?b=2&a=1`は異なるURLなので、ブラウザはこれをキャッシュしません。
でも、このようなクエリパラメータの順番違いに意味があることなんてまず無いですよね。

であれば、意味的に同じURLは形式的に異なっていてもキャッシュしてしまっていいのでは？

あと他所からのリンクにたまについてくる`utm_source=xxx`みたいなのはキャッシュには一切不要ですよね。
そういうのはクエリパラメータ自体を無視してキャッシュを使っちまおうぜ。

もちろん`https://localhost/?a=1`と`https://localhost/?a=2`は意味的に異なるURLなので、これにキャッシュを使ってはいけません。
また、並び順に意味があるサイトも万一存在するかもしれないので、全てのサイトで勝手にキャッシュするようにしてもいけません。

ということで、URLのクエリパラメータによるキャッシュを制御するHTTPレスポンスヘッダ`No-Vary-Search`が提唱されています。

提唱されていますというか[Chrome121で既に実装されています](https://developer.chrome.com/blog/new-in-chrome-121)。

以下は該当のRFC、[The No-Vary-Search HTTP response header](https://github.com/WICG/nav-speculation/blob/main/no-vary-search.md)の紹介です。
細かい部分、特に他APIとの相互作用などは<strike>面倒だった</strike>微に入りすぎて全体的な理解に邪魔だったので適当に端折ってます。
気になる人は原文読んでください。


# The No-Vary-Search HTTP response header

キャッシュは、Webページの読み込みを高速化し、ユーザエクスペリエンスの向上に役立ちます。
Webプラットフォーム上の主なキャッシュとしては、HTTPキャッシュのほか、このリポジトリのメインであるプリフェッチキャッシュやプリレンダキャッシュなどが存在します。

Webリソースをキャッシュするために最もよく使われるキーのひとつが、URLです。
ただし、同じリソースが複数のURLで示される場合があり、その場合はキャッシュは有効ではありません。

このRFCでは、この問題のよくある例のひとつである、一部のクエリパラメータのみが異なる複数のURLという場合に取り組みます。
新しいHTTPヘッダ`No-Vary-Search`では、キャッシュの区別を行うためにクエリパラメータの一部もしくは全てを無視できることを宣言します。
たとえばクエリパラメータの順序を気にしなくていい場合、次のように指定します。

```js
No-Vary-Search: key-order
```

一部のクエリパラメータを無視する場合は次のように指定します。

```js
No-Vary-Search: params=("utm_source" "utm_medium" "utm_campaign")
```

逆に、ホワイトリスト形式で一部のクエリパラメータだけを有効にしたい場合は次のように指定します。

```js
No-Vary-Search: params, except=("productId")
```

## Goals

クエリパラメータの順序によって、無駄にキャッシュが使われないことを防ぐ。

一部のクエリパラメータを無視することで、無駄にキャッシュが使われないことを防ぐ。

一部のクエリパラメータだけを有効にすることで、無駄にキャッシュが使われないことを防ぐ。

## Non-goals

それ以上の複雑なルールには対応しません。
たとえばキー`key`と`KEY`を同じとみなす、値`valu1`と`value2`を同じとみなす、一部のクエリパラメータだけ順序が決まっていてそれ以外は自由、といった複雑な条件は本RFCの対象外です。

標準ではないクエリパラメータの構造には対応しません。
`&`のかわりに`;`を使うなどの文法は認められません。

クエリパラメータ以外のURLについては対応しません。
URLパスなどに同じ仕様を導入することは有用かもしれませんが、本RFCでは対象外です。


## Prior art

このRFCは、既存のHTTPヘッダ[Vary](https://httpwg.org/specs/rfc9110.html#field.vary)からアイデアを得ています。
これはHTTPレスポンスヘッダによってキャッシュするかしないかを指示するヘッダです。
ヘッダとちがってレスポンスはURLによって異なるのが普通なので、`No-Vary-`接頭辞を用いています。

既存の`Vary`と本提案の`No-Vary-Search`はキャッシュ構築メカニズムの中心であり、両方がサポートされることが理想です。
しかし、`No-Vary-Search`がサポートされない場合は単にキャッシュヒット率が低くなるだけなので、直ちに`No-Vary-Search`をサポートするようにアップグレードすることは必須ではありません。

クエリパラメータが適切なキャッシュ戦略を妨害していることは既に広く認識されており、一部のCDNは独自の対策をとっています。
たとえば[CloudFlare](https://developers.cloudflare.com/cache/about/cache-keys)は、`include`・`exclude`を指定することで一部のクエリパラメータを無視することができます。
また[Amazon CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/QueryStringParameters.html#query-string-parameters-optimizing-caching)ではホワイトリスト形式でキャッシュするクエリパラメータを選択できます。
[`cache vary query parameters`](https://www.google.com/search?q=cache+vary+query+parameters)を検索すると、様々な技術文書が見つかります。
我々の提案はブラウザを対象としていますが、CND・プロキシやその他のHTTPエコシステムも、このヘッダを利用できるだろうと楽観視しています。

最後に、ServiceWorkerなどでよく使われる[Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)はVaryヘッダと[ignoreSearch](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match)オプションを組み合わせることで、キャッシュ制御をうまく行うことができます。
この方法で`No-Vary-Search`とキャッシュAPIの機能を統合する予定です。


## Use cases

クエリパラメータで情報を渡したいが、キャッシュはしてほしいような例が見つかります。

### Avoiding unnecessary cache mismatches due to inconsistent referrers

リファラーがダミーのクエリパラメータを追加したり、クエリパラメータの順番が同じにならなかったりすることがあります。
これによって不要なキャッシュミスが発生します。
リンク元が自らが所有するサイトであれば修正できますが、外部サイトだった場合は修正できません。
また自身のものであっても、様々な事情で修正が難しい場合も考えられます。

### Customizing server behavior

アプリによってはクエリパラメータでサーバの動作を制御している場合があります。
たとえば負荷分散環境においてリクエスト先を特定のインスタンスに指定するとか、ログの出力を制御するとか、優先度を変更するとかです。
このような情報は本来リクエストヘッダを使うのが理想ですが、aタグなどを使っていてリクエストヘッダのカスタマイズが困難な場合もあります。


### Carrying data that is or can be processed by client-side script only

クライアント側スクリプトだけで処理できるデータなどの扱い。
ログインプロセスや地図座標、強調表示したい製品など、これらのデータをクライアントに渡したい場合、そのようなデータはフラグメントで渡しますが、現実的にはフラグメントのかわりにクエリパラメータが使われることが多々あります。
さらにプリロードによって先読みする場合、サーバ側で処理を走らせるかわりにクライアント側で処理させることでプリロードの利点が発揮されます。

もうひとつの使用例は、完全にクライアント側でレンダリングされるスケルトンページです。
ただしこの場合、クエリパラメータではなくパスが使われることも多く、そして本RFCではパスを対象としていません。
将来の展望を参照してください。


### Carrying data not yet determined at the time of preloading

最もわかりやすい例は分析です。

```js
<script type="speculationrules">
{
  "prerender": [{
    "source": "list",
    "urls": ["/articles/new-underwater-phone"]
  }]
}
</script>

<a href="/articles/new-underwater-phone?via=heroimage">
  <img src="underwaterphone.jpg" alt="A phone, underwater!">
</a>

<a href="/articles/new-underwater-phone?via=headline">New underwater phone, just released!</a>
```

ページが読み込まれた時点では、ユーザは画像をクリックするか文字列をクリックするかはわかりません。
従って、クエリパラメータを使用せずにページをプリレンダリングしておき、リンクをクリックしてからその情報を伝達させるといった方法が便利です。

## Detailed design

### The header

`No-Vary-Search`は[dictonary](https://httpwg.org/specs/rfc8941.html#dictionary)形式の[HTTP Structured Field](https://www.rfc-editor.org/rfc/rfc8941.html)です。
以下のキーが規定されています。

`params`：全てのクエリパラメータを無視する[boolean](https://httpwg.org/specs/rfc8941.html#boolean)、もしくは無視するクエリパラメータを列挙したリストです。

`expect`：paramsがtrueの場合のみ設定可能で、無視しないクエリパラメータを列挙するリストです。

`key-order`：クエリパラメータの順序を気にしなくていい場合に指定します。

不明もしくは無効な項目が見つかった場合は、`No-Vary-Search`ヘッダ自体が設定されていなかったものとして扱われます。
これによりキャッシュミスは増えますが、誤ったキャッシュヒットよりは害が少ないためデフォルトとして採用されます。

以下に設定例をいくつか示します。

```js
No-Vary-Search: params // 全てのクエリパラメータを無視する
No-Vary-Search: key-order // 順番を無視する
No-Vary-Search: params=("utm_source") // utm_sourceだけ無視する
No-Vary-Search: key-order, params, except=("productId") // productId以外を無視する
```

### Navigated-to pages

`No-Vary-Search`によってキャッシュされたリソースと一致するURLに移動しようとした場合、キャッシュされたURLではなく、ターゲットのURLがキャッシュされているかのように振る舞います。

すなわち、Service WorkerやResource Timing APIは、ターゲットURLへのfetchを実行します。
aタグからページ遷移後のlocation.hrefは、ターゲットのURLです。
サブリソース取得時のRefererヘッダは、ターゲットのURLです。

言い換えると、この提案はキャッシュレイヤーでのURLマッチングの仕組みを変更するものといえます。


### Interaction with redirects …

URLを扱うメカニズムは、リクエストURL・レスポンスURLについて正確に取り扱う必要があります。
しかしリダイレクトが関係する場合は扱いが変わる可能性があります。

#### … at the HTTP and Cache API level

HTTP cacheは、個々のキャッシュルックアップごとにリクエストURLとレスポンスURLは完全に同じです。
`No-Vary-Search`を使ったとしてもこれは同じです。
`No-Vary-Search`を使うと、もともとひとつのURL専用だったキャッシュを、異なるURLでも使用できるようになります。

例として、`/apple-watch-ulatr`へリクエストしてみることにします。

まずキャッシュ内に`No-Vary-Search: params=("utm_source")`が指定された`/apple-watch-ulatr?utm_source=homepage`が見つかりました。
ステータスコードは301で、中身は`Location: /apple-watch-ultra?utm_source=homepage`でした。
次に`/apple-watch-ultra?utm_source=homepage`にリクエストしようとしたら、キャッシュに`No-Vary-Search: params=("utm_source")`が指定された`/apple-watch-ultra?utm_source=twitter`が見つかったので、こちらが返ってきました。

ユーザのURLに表示される内容は、`No-Vary-Search`を使わなかった場合と異なる可能性があります。
`No-Vary-Search`を使わなかった場合、URLには`/apple-watch-ulatr`からリダイレクトされた先の`/apple-watch-ultra`が表示されるでしょう。

このサイトでは、正確なURLと正確なutm_sourceを、キャッシュの使用率向上のために犠牲にしています。
このような動作が好ましくない場合は、`No-Vary-Search`を使わないようにするか、`history.replaceState()`でURLを変更することができます。

同じことが、[Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)にも当てはまります。


## Alternatives considered

検討された代替案。

`<link rel="canonical">`および`Link`ヘッダを拡張することで、ここに示した例の一部はカバーできます。
しかし、既存の`<link rel="canonical">`の広い使われ方を考えると、拡張には互換性の問題が発生する可能性があります。
また`/?k=v`が`/`と同じことを示すには使えますが、`/?k=v1`と`/?k=v2`が同じことを表せないなど制限があります。

既存の`Vary`ヘッダと揃えて`Vary-Search`ヘッダにすることもできます。
しかしあえて`No`の接頭辞を選択した理由は、値が空白のデフォルト状態の動作を、未指定時の既存のHTTPキャッシュの動作(つまり、全てのクエリパラメータで異なる)と揃えるためです。
`Vary-Search`にしてしまうと、値が空の状態と、ヘッダ自体が存在しない状態で動作が異なってしまいます。

ヘッダ名としてはほかに`No-Vary-Query`などが考えられました。
`Search`にした理由は、`location.search`・`url.search`・`url.searchParams`・`URLSearchParams`といったAPIが既にWebに存在するためです。



## Extensibility

このRFCは、この機能の初期提案項目すべてをカバーしています。
しかし、今後の機能拡張の可能性が多数模索されています。

### More complex no-vary rules

さらに複雑なルールのサポート。

値の大文字小文字を区別しないようにするオプション`value-case-insensitive`。

```js
No-Vary-Search: params=("color";value-case-insensitive=?1)
```

同じとみなす値を正規表現で決めるオプション`value-regexp`。

```js
No-Vary-Search: params=("color";value-regexp="(?:blue|azure)")
```

値の順序を気にしないオプション`value-order`。
たとえば`?x=y&x=z`と`?x=z&x=y`を同じとみなす。


これらは、本RFCにおける提案よりもユースケースが少ないと考えられるため、今のところ対象としていません。
しかし、将来的に拡張の余地が存在するのはよいことです。


### No-Vary-Path

クエリパラメータのキャッシュミスを回避する策の多くは、パスについても当てはまります。
特にSPAにおいては顕著です。
たとえば`/products/123`と`/products/456`はいずれも、製品ページのスケルトンとなるHTTPレスポンス自体は同一で、中身のコンテンツだけがクライアント側で別途構築されます。

URLの部分がクエリパラメータとして構成されていれば、このような例も`No-Vary-Search`で対応可能ですが、パスについては対象外です。
このユースケースは、考え方としては`No-Vary-Search`に似ているので、同様の追加によって適切に対処できると考えています。
この仮想的な提案をひとまず`No-Vary-Path`と呼びましょう。

### A <meta> version

HTTPヘッダを追加するのが難しい場合もあり、マークアップで解決したいという需要もあるでしょう。
これに対応する最も自然な方法は、`<meta http-equiv="No-Vary-Search">`です。

ただし、様々な影響が考えられるため周到な調査が必要です。
たとえば、HTTP chacheがHTMLをパースするという提案は、我々の知るかぎりこれが初めてです。

この方法を使わず、レスポンスヘッダで対応することをお勧めします。
それでもどうしても必要があれば、このオプションを再検討するかもしれません。


## Security and privacy considerations

セキュリティ面で注意すべき主なリスクは、URLの不一致による影響です。
リンク上にマウスを置いたときに表示されるURL、およびURL欄に表示されるURLと実際には異なるURLから取ってきていた値が表示されることがあります。

もっとも、これが影響するのはクエリパラメータだけなので、セキュリティ境界を超えることはないでしょう。

本RFCは、ユーザの追跡にクエリパラメータを使うといった、プライバシーに関連性の高い領域に属しています。
しかし、この提案自体がプライバシーに影響を与えることはないと思われます。
このRFCは、既存および今後予定されているユーザ追跡の緩和策を妨げることはありません。

実際WebページがURLにユーザ識別子を埋め込んでいるとして、このRFCができることは、サーバにリクエストを送らないようにしてユーザ追跡の機会を減らすことです。


# 感想

`No-Vary-Search`は今のところローカルファイルのキャッシュに使われる機能です。
つまり、少なくとも一回は見たことのあるページにしか使われないので、うっかり下手な設定をしてしまっても見れてはいけないものが見れてしまうなんてことは起きません。
ただ設定をミスると、Aさんのデータを見ようとしたのにBさんのが表示されていてそのまま更新したらデータがバグったみたいなことはあるかもしれませんが。

またRFCではCDNでも使えるのでは的なことが書いてありますが、これをやってしまうと[事故る](https://engineering.mercari.com/blog/entry/2017-06-22-204500/)未来しか見えません。

あと将来の展望のところ、`value-case-insensitive=?1`に`?`が入っているのは原文からなのですがこれは間違いのような気がしてならない。


さて本RFCはGoogleが提唱してGoogleが実装した、よくあるChrome独自実装のひとつです。
とはいえ邪悪な目的で使うことは困難な仕様であり、またうまく使えば多くのデータをキャッシュで済ますことができるようになるので、使いこなせばかなり便利な機能なのではないかと思います。
ただまあ、これを使っているところを見たことが、というかこの機能が話題になったところすら見たことありませんが。
なにしろGoogleのサイトですら使われていないようです。

また他ブラウザはというと、[Firefox](https://github.com/mozilla/standards-positions/issues/717)・[Safri](https://github.com/WebKit/standards-positions/issues/106)ともに目立った反応がありません。

ということで、いつものようにChromeの独自実装で終わりそうな気がしないでもないです。