7月 252022
 

はじめに

少し前に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.
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.

8. HTTP Message Exchanges RFC7540

これが

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上で行われた議論を読んでみるとなんでこの文面が変わったのかというのがわかって納得感が違いますのでお勧めです。

https://github.com/httpwg/http-core/issues?q=is%3Aissue+is%3Aclosed+label%3Acaching+sort%3Acreated-asc

例えば今までは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でと覚えておけばよいかと思います。

議論:UTC in dates #472

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参照)、検証要求のリトライをすることができる。

となりました。

議論:Validation failures #462

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=0no-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-Controlno-storeが含まれていない
  • 経路上のキャッシュ(shared)として格納しようとしている際、レスポンスのCache-Controlprivateの指定がないこと
  • 経路上のキャッシュ(shared)として格納しようとしている際、リクエストにAuthorizationヘッダが含まれていないこと、ただし明示的に許可している場合は除く
  • レスポンスで以下の条件のうちどれかを満たす
    • Expiresヘッダを含む
    • Cache-Control内にmax-ageを含む
    • 経路上のキャッシュ(shared)として格納しようとしている際、Cache-Controls-maxageを含む
    • 拡張ディレクティブで明示的に許可されているもの
    • ステータスコードがデフォルトでキャッシュ可能なもの
    • Cache-Control内にpublicを含む

以上の条件を満たす場合はキャッシュされます。

でした。

これがRFC9111では以下のように変更されています。

  • リクエストメソッドが解釈できるもの
  • ステータスコードは最終応答である
  • ステータスコードが206304、もしくはCache-Controlmust-understandを含む場合、ステータスコードが解釈できるもの
  • レスポンスのCache-Controlno-storeが含まれていない
  • 経路上のキャッシュ(shared)として格納しようとしている際、Cache-Controlprivateの指定がない。もしくはprivateでフィールド名の指定があり、それに従い改変されたレスポンスである
  • 経路上のキャッシュ(shared)として格納しようとしている際、リクエストにAuthorizationヘッダが含まれていないこと、ただし明示的に経路上のキャッシュに格納することを許可している場合は除く
  • レスポンスで以下の条件のうちどれかを満たす
    • Cache-Control内にpublicを含む
    • 格納先が経路上のキャッシュ(shared)でない場合、Cache-Control内にprivateを含む
    • Expiresヘッダを含む
    • Cache-Control内にmax-ageを含む
    • 経路上のキャッシュ(shared)として格納しようとしている際、Cache-Controls-maxageを含む
    • 拡張ディレクティブで明示的に許可されているもの
    • ヒューリスティックにキャッシュ可能なステータスコード

以上の条件を満たす場合はキャッシュされます。

なお最終応答についてはRFC9110#15を参照してほしいのですが、非最終応答が1xxでそれ以外が最終応答となります。

また、no-storeが含まれていないことの条件ですが、must-understandと同時に指定されていてキャッシュ条件を満たしているのであれば無視できます(先ほど触れたmust-understand参照)

個人的にはこれもここに含めておけばよかったんじゃ・・とも思います。

4 キャッシュからの応答の構築法

こちらはほぼ変更がありません。

せいぜいリクエスト時のcache-controlやPragmaの記述が消えたぐらいです。

  • リクエストとキャッシュのURIが一致すること
  • キャッシュ格納時のメソッドがリクエストのメソッドで使えることを許容していること
  • キャッシュ中にVaryでヘッダが指定されている場合は、キャッシュ中のセカンダリキーとリクエストの指定ヘッダの値が一致すること
  • リクエスト中のCache-ControlPragma内にno-cacheを含む場合はキャッシュの検証が成功していること(RFC9111ではこの条件が消えた)
  • キャッシュ中のCache-Control内にno-cacheを含む場合はキャッシュの検証が成功していること
  • キャッシュの状態が以下のどれかである
    • Fresh
    • Staleでの利用が許容されている
    • 検証が成功している

最後に

他にも変更要素がありますので是非Appendix B. Changes from RFC 7234を参照してみてください。


 Posted by at 10:55 PM

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください