はじめに
少し前にHTTP関連のRFCが改訂されましたね!(RFC91XX)
このあたりの全般的な話は既に詳しい方々が触れていますので(HTTP 関連 RFC が大量に出た話と 3 行まとめ)、基本的にはそっちを参照すればよいかなと思うのですが、何が変わったかというと大規模なリファクタリングがされたといっていいでしょう。(もちろん同時にHTTP/3やCache-Statusなどの新しい仕様もふえています)
とはいえHTTPのリファクタリングってなに?と思うことも多いと思うのでHTTP/2の例示をしてみます。
皆さんご存じの通りHTTP/2、HTTP/1.1のどちらにおいてもAccept-Encoding
などのフィールド(改訂でヘッダからフィールドに用語が変わりました)は同様に使えます。
要はセマンティクスは同じなわけですが、RFC7231ではHypertext Transfer Protocol (HTTP/1.1): Semantics and Content
とあるとおりHTTP/1.1と深く紐づいていました。
今回の改訂ではセマンティクスはRFC9110にまとめられましたが、このことによりHTTP/2などのRFCが非常にすっきりしました。
どれぐらいすっきりしたかがわかるようにHTTP/2のSection 8を比較してみましょう。
どちらもセマンティクスについての記述です。
HTTP/2 is intended to be as compatible as possible with current uses of HTTP. This means that, from the application perspective, the features of the protocol are largely unchanged. To achieve this, all request and response semantics are preserved, although the syntax of conveying those semantics has changed.
8. HTTP Message Exchanges RFC7540
Thus, the specification and requirements of HTTP/1.1 Semantics and Content [RFC7231], Conditional Requests [RFC7232], Range Requests [RFC7233], Caching [RFC7234], and Authentication [RFC7235] are applicable to HTTP/2. Selected portions of HTTP/1.1 Message Syntax and Routing [RFC7230], such as the HTTP and HTTPS URI schemes, are also applicable in HTTP/2, but the expression of those semantics for this protocol are defined in the sections below.
これが
HTTP/2 is an instantiation of the HTTP message abstraction (Section 6 of [HTTP]).
8. Expressing HTTP Semantics in HTTP/2 RFC9113
こんな感じです。
もちろんリファクタリングだけではありません。
RFC9111になったHTTP Cachingでは新要素(must-understand)や使われなくなった部分や曖昧な部分の整理が行われています。
つまりは今までキャッシュを知っている人からするとまぁそんな変わらないよとなるわけですが、ここで重要なのは「曖昧な部分の整理」となります。
RFC7234を読むとわかると思うのですが、この場合はどう解釈すればいいのだろう・・と詰まるところは多数あると思います。
no-cacheとmax-ageが同時に指定されていた場合はどちらが優先されるのでしょうか?
no-cacheが指定されている場合は再検証が必要ですが、有効期限内のキャッシュは果たして再検証の対象となるのでしょうか?
とか考えだすともにょもにょしだすわけです(個人的にはRFC7234でもno-cacheが優先されると考えています)
今回の改訂ではこのような部分がだいぶ解消されていますので、なんとなく引っかかったポイントを解説します。
なお、紹介する順序は細かいところからとしているのと、ある程度キャッシュの知識がある人向けに書いています。(Web配信の技術をざっと読んでれば大丈夫だと思うので買ってね!(唐突な宣伝))
改訂の(おすすめな)読み方
今回触れるのは変更の一部ですので、是非皆さんも読んでほしいのですが
その際には単純に新旧のRFCを読むだけではなく、Github上で行われた議論を読んでみるとなんでこの文面が変わったのかというのがわかって納得感が違いますのでお勧めです。
例えば今まではmax-age=”3600″(quoteされてる)といったものはSHOULD NOTだったのですが、より強いMUST NOTに変わりました。
個人的にはquoteするような実装があるのかと思ってたんですが、議論を見ると白熱しており、なるほどなぁと思いました。
議論:Quoted cache-control directives #128
4.2 有効期限の計算に使うのはGMTのみに変更
元々RFC2616#19.3では有効期限の計算はGMTで行う必要があります(MUST) GMT以外のタイムゾーンを誤って指定された場合は最も保守的な変換を使いGMTに変換する必要があります(MUST) とありました。(意訳)
それがRFC7234#4.2では有効期限の計算においてGMTもしくはUTC以外のタイムゾーン指定は無効とみなすべきです(SHOULD)となりました(意訳)
ところがUTCをサポートしている実装は少なく、基本はGMTですのでGMTのみとなりました。そう混乱するような変更ではないと思いますが、まぁ時刻はGMTでと覚えておけばよいかと思います。
5.4 Pragma: no-cacheが非推奨
Pragma: no-cache
はそもそもHTTP/1.0時代のもので、既に広くCache-Controlが普及したので非推奨となりました。
議論: Pragma #140
5.5 Warningの廃止
余り知られてないフィールドにWarningというのがありました。 要は何か伝えたいことがあるときに使うもので、
Warning: 112 - "network down" "Sat, 25 Aug 2012 23:34:45 GMT"
こんな感じのものですが使われてないですし、他をみればわかるし不要ですよねってことで廃止となりました。
議論:Warning #139
4.3.3 検証中に5xxが返ってきた場合はリトライできることを記載
タイトルまんまですが、検証時にオリジンからのレスポンスがサーバーエラー(5xx)の場合の動きとして
- (OR)リクエスト元にこの応答(5xx)を転送する
- (OR)サーバが応答できなかったものとして動作を行う
- この場合は以前保存した応答を制約のもとにレスポンスするか(要はStaleキャッシュのレスポンス4.2.4参照)、検証要求のリトライをすることができる。
となりました。
4.2.1 重複定義・競合定義の取り扱い
例えばExpiresが複数存在していたり、Cache-Control内にmax-ageが複数存在していた場合はどうすればよいでしょうか?
RFC7234#4.2.1ではディレクティブの値は無効であるとみなされる。無効な情報を持つキャッシュはStaleとして扱うことを推奨するとあります。 (この記述は特にMUST/SHOULD/MAYでもなくencouraged)
ではno-storeが複数定義されている場合はどうでしょうか?
そのまま捉えてしまうと、Staleとして扱うことは再検証すれば利用ができる状態なため、no-storeの期待している動作より緩い指定です。
そこで今回この辺りの取り扱いが整理されました。
- 重複定義(複数のExpiresやmax-age)は最初に出現したものを利用するか、応答自体をStaleとみなします。
- 競合定義(max-ageとno-cacheの同時指定など)は最も制限的なディレクティブを尊重する必要があります。(この場合はno-cache)
- キャッシュは無効な鮮度情報(max-ageに非整数など)をもつ場合はStaleとして扱うことを推奨する。
わかりやすくてよいかと思います。
議論:Handling duplicate directives (part II) #460
5.1 Ageヘッダの解釈を整理
Ageヘッダは基本的に1つのみの定義です。
ところがDevToolを開きながら見ているとたまにAgeが重複定義されていたり、リスト(X, XX)の形で折りたたまれているケースがあります。
この場合の解釈では最初のものを利用し、それ以降のものは破棄する必要がある(SHOULD)と記載されました。
議論を見てもらえばわかるのですが、これは既存実装を参考にして明確にしただけですので、そう混乱はないかと思います。
議論:Revisiting Age error handling #471
5.2.1 要求ディレクティブ全体がMAYに変更
Cache-Controlは応答時によく見かけますが、要求時にも発行されています。
実際ブラウザでリロードを行うとmax-age=0
やno-cache
が発行されますが、CDNやProxyでは解釈されないことがほとんどです。(HTTPキャッシュ入門の入門)
ですが、単純に仕様を読む限りではMUSTであったりするので非常に解釈が難しい部分でした。 今回、ここに文言が追加されました。
This section defines cache request directives. They are advisory; caches MAY implement them, but are not required to.
(意訳)このセクションでは要求ディレクティブを定義します。これらの実装は必須ではありません。(実装してもよい(MAY))
要はまるっと要求ディレクティブが実装は必須でもないよとなったわけです。
これに伴い他の箇所で(具体的には後で触れるキャッシュ保存のフローなど)要求ディレクティブに関する記述が消えているところがあります。
ですが、考慮する必要がなくなったというわけではなく、要求ディレクティブに対応する場合は当然対応が必要となるので気を付ける必要があります。
議論: Request Cache-Control directives #129
5.2.2.3 must-understandディレクティブの追加
詳しくはjxckさんが書かれていることと同じでより詳しいのでそちらを見てほしいのですが(Cache-Control: must-understand ディレクティブとは何か )
まずRFC7234#3のキャッシュする条件においてthe response status code is understood by the cache(ステータスコードを理解していないとキャッシュできない)
があります。
例えばステータスコード999
が爆誕して、このステータスコードはデフォルトでキャッシュが可能(ステータスコード200
のように)とします。
この状態でcache-control: max-age=60
と指定したとしても、999を解釈できない実装(CDNやProxy)はキャッシュ条件に従ってキャッシュをしません。
この動きは安全ではあるものの、保守的です。
一般的に新しい仕様が広く実装されるまでには時間がかかります。
適切なcache-controlの指定(max-age指定)があったとしても、未知のステータスコードというだけでキャッシュができないというのはちょっと厳しすぎないか?というところから始まってます。
とはいえ、単純に「ステータスコードを理解していないとキャッシュできない」という制限を外してしまうと未知のステータスコードが何らかの条件の元にキャッシュが可能な場合、誤ってキャッシュしてしまい事故につながる可能性があります。
既にあるステータスコードで何か例があるのかというと、Rangeリクエストの結果で返ってくる206やIMS/INMリクエストで返ってくる304です。
どちらもCache-Controlでmax-ageを指定される可能性はありますが、どちらも正しく解釈できないとキャッシュすることができません。
例えばrangeで0~1KBまでリクエストして、206で返ってきた一部のコンテンツをキャッシュしたとします。次に1~2KBのrangeリクエストがあったときに、0~1KBの範囲の結果を返したらコンテンツが壊れてしまいます。
つまり理解できないとキャッシュができないという状態です。
そこでできたのがmust-understand
です。
must-understandが指定されている場合はそのキャッシュ要件を理解していないといけないという指定になります。
つまりは先ほどの206や304のようにキャッシュするための要件があるから、例えmax-ageが指定されていたとしてもキャッシュしてはいけないといった指定になります。
とはいえ、must-understandは新しいディレクティブです。このディレクティブ自体が普及してない状態でそんなこと言われても・・・となります。
そこで、同時にno-storeを指定する必要がある(SHOULD)と記載されています。
cache-control: max-age=60, must-understand, no-store
たとえばこのような指定をした場合で、must-understandを理解しない実装の場合は拡張ディレクティブ扱いとなります(RFC7234#5.2.3, RFC9111#5.2.3) 理解できない拡張ディレクティブの場合は単に無視するだけですので
cache-control: max-age=60, no-store
といった指定になります。 この場合no-storeが指定されているため、キャッシュされないので安全です。
じゃあmust-understandもステータスコードのキャッシュ要件を理解して実装している場合はどうなるのかというと
きちんと仕様上にno-storeを無視する必要がある(SHOULD)と書かれています。
議論: Status codes and caching #120
3.1 ヘッダ・トレイラーの取り扱いについて
これはどちらかというとセマンティクス側(RFC9110#6.5)の項目なんですがこちらにも記載があるので注意事項として取り上げます。
フィールドにはレスポンスのはじめに送られてくるヘッダ(Header)フィールドとボディを送った後にトレイラー(Trailer)フィールドがあります。(詳しくはflano_yukiさんの“HTTPヘッダ”が指すものとは参照)
トレイラーが送られてきた際のキャッシュの動きとして
- 別々に保管(ヘッダとトレイラーを別物として扱う)するか破棄してもよい(MAY)
- ヘッダとトレイラーを組み合わせてはいけません(MUST NOT)
となります。
要はどこで送られたかというのが重要なユースケースもあるんで勝手にマージしてそれがヘッダ由来なのかトレイラー由来なのかわからない状態にすんなということだと思います。
この辺りはRFC9110#6.5あたりを読んでみるとよいかと思います。
3 キャッシュを保存するときの条件
当然ですが特に破壊的な変更はありません。
ポイントとしては、must-understandとリクエスト時のcache-controlが条件から消えている(先ほど触れたようにMAYに変更されたので)ところかなと思います。
後はRFC7234においても他の箇所を読むと書いてあるけど記載されてないものを明記したというところです。(特にprivateの追記がポイントかなと)
RFC7234の時は
- リクエストメソッドが解釈できるもので、かつキャッシュ可能なメソッドとして定義されている(キャッシュ可能なメソッドを参照)
- ステータスコードが解釈できるもの
- リクエスト・レスポンスの
Cache-Control
にno-store
が含まれていない - 経路上のキャッシュ(shared)として格納しようとしている際、レスポンスの
Cache-Control
にprivate
の指定がないこと - 経路上のキャッシュ(shared)として格納しようとしている際、リクエストに
Authorization
ヘッダが含まれていないこと、ただし明示的に許可している場合は除く - レスポンスで以下の条件のうちどれかを満たす
Expires
ヘッダを含むCache-Control
内にmax-age
を含む- 経路上のキャッシュ(shared)として格納しようとしている際、
Cache-Control
にs-maxage
を含む - 拡張ディレクティブで明示的に許可されているもの
- ステータスコードがデフォルトでキャッシュ可能なもの
Cache-Control
内にpublic
を含む
以上の条件を満たす場合はキャッシュされます。
でした。
これがRFC9111では以下のように変更されています。
- リクエストメソッドが解釈できるもの
- ステータスコードは最終応答である
- ステータスコードが
206
か304
、もしくはCache-Control
にmust-understand
を含む場合、ステータスコードが解釈できるもの - レスポンスの
Cache-Control
にno-store
が含まれていない - 経路上のキャッシュ(shared)として格納しようとしている際、
Cache-Control
にprivate
の指定がない。もしくはprivate
でフィールド名の指定があり、それに従い改変されたレスポンスである - 経路上のキャッシュ(shared)として格納しようとしている際、リクエストに
Authorization
ヘッダが含まれていないこと、ただし明示的に経路上のキャッシュに格納することを許可している場合は除く - レスポンスで以下の条件のうちどれかを満たす
Cache-Control
内にpublic
を含む- 格納先が経路上のキャッシュ(shared)
でない
場合、Cache-Control
内にprivate
を含む Expires
ヘッダを含むCache-Control
内にmax-age
を含む- 経路上のキャッシュ(shared)として格納しようとしている際、
Cache-Control
にs-maxage
を含む - 拡張ディレクティブで明示的に許可されているもの
- ヒューリスティックにキャッシュ可能なステータスコード
以上の条件を満たす場合はキャッシュされます。
なお最終応答についてはRFC9110#15を参照してほしいのですが、非最終応答が1xxでそれ以外が最終応答となります。
また、no-storeが含まれていないことの条件ですが、must-understandと同時に指定されていてキャッシュ条件を満たしているのであれば無視できます(先ほど触れたmust-understand参照)
個人的にはこれもここに含めておけばよかったんじゃ・・とも思います。
4 キャッシュからの応答の構築法
こちらはほぼ変更がありません。
せいぜいリクエスト時のcache-controlやPragmaの記述が消えたぐらいです。
- リクエストとキャッシュのURIが一致すること
- キャッシュ格納時のメソッドがリクエストのメソッドで使えることを許容していること
- キャッシュ中に
Vary
でヘッダが指定されている場合は、キャッシュ中のセカンダリキーとリクエストの指定ヘッダの値が一致すること - リクエスト中の
Cache-Control
かPragma
内にno-cache
を含む場合はキャッシュの検証が成功していること(RFC9111ではこの条件が消えた) - キャッシュ中の
Cache-Control
内にno-cache
を含む場合はキャッシュの検証が成功していること - キャッシュの状態が以下のどれかである
- Fresh
- Staleでの利用が許容されている
- 検証が成功している
最後に
他にも変更要素がありますので是非Appendix B. Changes from RFC 7234を参照してみてください。