[インデックス 11836] ファイルの概要
このコミットは、Go言語のnet/httpパッケージにおけるHTTPキャッシュの振る舞いを改善するものです。具体的には、If-Modified-Sinceヘッダーとファイルの最終更新時刻(mtime)の比較ロジックを修正し、秒以下の精度が切り捨てられることによる問題を解決しています。これにより、HTTPクライアントがリソースが変更されていないと誤って判断し、古いキャッシュされたコンテンツを提供してしまう可能性を低減します。
コミット
- Author: Hong Ruiqi hongruiqi@gmail.com
- Date: Sun Feb 12 23:45:19 2012 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c58b6ad02203cc0f4ba3cd0a38ce222d177cc75e
元コミット内容
net/http: use mtime < t+1s to check for unmodified
The Date-Modified header truncates sub-second precision, so
use mtime < t+1s instead of mtime <= t to check for unmodified.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5655052
変更の背景
HTTPプロトコルでは、クライアントがリソースのキャッシュを効率的に利用するために、If-Modified-SinceヘッダーとLast-Modifiedヘッダーが使用されます。サーバーはリソースの最終更新時刻をLast-Modifiedヘッダーに含めてレスポンスを返します。次にクライアントが同じリソースをリクエストする際、以前受け取ったLast-Modifiedの値をIf-Modified-Sinceヘッダーに含めて送信します。サーバーは、このIf-Modified-Sinceの値とリソースの現在の最終更新時刻を比較し、リソースが変更されていなければ304 Not Modifiedステータスを返して、クライアントにキャッシュされたコンテンツを使用するよう指示します。
このメカニズムには、日付/時刻の精度に関する潜在的な問題があります。HTTPヘッダー(特にDateやLast-Modified)で表現される時刻は、通常、秒単位の精度に切り捨てられます。しかし、ファイルシステムの最終更新時刻(mtime)は、多くの場合、秒以下の精度(ミリ秒やナノ秒)を持っています。
元のコードでは、If-Modified-Sinceヘッダーからパースされた時刻tと、ファイルの実際の最終更新時刻modtimeをmodtime <= tという条件で比較していました。この比較が問題となるのは、ファイルがtの直後に、しかし同じ秒内に更新された場合です。例えば、modtimeが2012-02-12 23:45:19.500で、tが2012-02-12 23:45:19.000(HTTPヘッダーで秒以下が切り捨てられた結果)だったとします。この場合、modtime <= tは偽となり、サーバーはリソースが変更されたと判断して200 OKレスポンスを返します。しかし、実際にはファイルはtの秒内に更新されており、クライアントがIf-Modified-Sinceで送った時刻は、その更新をカバーしているべきです。
このコミットは、この秒以下の精度に関する不一致を解消し、HTTPキャッシュの振る舞いをより堅牢にするために行われました。
前提知識の解説
- HTTPキャッシュ: Webパフォーマンスを向上させるための重要なメカニズム。クライアントが一度取得したリソースをローカルに保存し、次回以降のリクエストでサーバーへの負荷を減らし、表示速度を向上させます。
If-Modified-Sinceヘッダー: HTTPリクエストヘッダーの一つ。クライアントが、指定された日時以降にリソースが変更された場合にのみ、そのリソースを要求するために使用します。サーバーは、このヘッダーの値とリソースの最終更新時刻を比較します。Last-Modifiedヘッダー: HTTPレスポンスヘッダーの一つ。サーバーが、リソースの最終更新日時をクライアントに伝えるために使用します。304 Not Modifiedステータスコード: HTTPステータスコードの一つ。クライアントがIf-Modified-Sinceヘッダーを送信し、サーバーがリソースが指定された日時以降に変更されていないと判断した場合に返されます。この場合、レスポンスボディは空で、クライアントは自身のキャッシュからリソースを提供します。- ファイルシステムのmtime (modification time): ファイルが最後に変更された時刻を記録するメタデータ。多くの現代のファイルシステムでは、秒以下の精度(ミリ秒、マイクロ秒、ナノ秒など)で記録されます。
- Go言語の
time.Time型: Go言語で日時を扱うための型。ナノ秒までの精度をサポートしています。 time.Parse(layout, value string) (Time, error): 指定されたレイアウト(フォーマット)に従って文字列をtime.Time型にパースする関数。HTTPヘッダーの日付フォーマット(RFC1123など)をパースする際に使用されます。time.Time.Before(u Time) bool:Timeオブジェクトが引数uよりも前の時刻である場合にtrueを返します。time.Time.Add(d Duration) Time:Timeオブジェクトに指定された期間dを加算した新しいTimeオブジェクトを返します。
技術的詳細
このコミットの核心は、HTTPヘッダーにおける日付/時刻の精度と、ファイルシステムの最終更新時刻の精度との間の不一致をどのように扱うかという点にあります。
HTTP/1.1の仕様(RFC 2616, Section 3.3.1)では、日付/時刻のフォーマットは秒単位の精度を持つことが規定されています。これは、Last-ModifiedヘッダーやIf-Modified-Sinceヘッダーで送信される時刻が、秒以下の情報を持たないことを意味します。
一方、Go言語のtime.Time型や多くのファイルシステムは、秒以下の精度で時刻を扱います。例えば、ファイルが2012-02-12 23:45:19.500に更新されたとします。この時刻がLast-Modifiedヘッダーとして送信されると、2012-02-12 23:45:19に切り捨てられます。クライアントがこの値をIf-Modified-Sinceヘッダーに含めて再リクエストした場合、サーバーはIf-Modified-Sinceの値として2012-02-12 23:45:19を受け取ります。
ここで問題となるのは、ファイルが2012-02-12 23:45:19.100に更新された場合です。このmodtimeは、If-Modified-Sinceで受け取ったt(2012-02-12 23:45:19.000)よりも厳密には後ですが、秒単位で見ると同じです。元のロジックmodtime <= tでは、modtimeがtより厳密に後であるため、falseとなり、304 Not Modifiedが返されません。これは、クライアントがキャッシュを更新すべきであるにもかかわらず、サーバーが変更がないと誤って判断してしまう状況を生み出します。
このコミットは、この問題を解決するために比較ロジックをmodtime < t+1sに変更しました。
t.Add(1*time.Second)は、If-Modified-Sinceヘッダーからパースされた時刻tに1秒を加算します。これにより、tが2012-02-12 23:45:19.000であれば、t.Add(1*time.Second)は2012-02-12 23:45:20.000となります。
新しい条件modtime.Before(t.Add(1*time.Second))は、ファイルの最終更新時刻modtimeが、If-Modified-Sinceヘッダーの時刻tの「次の秒の開始時刻」よりも前であるかどうかをチェックします。
これにより、modtimeがtと同じ秒内(例えば2012-02-12 23:45:19.100や2012-02-12 23:45:19.999)に更新された場合でも、modtimeはt.Add(1*time.Second)(2012-02-12 23:45:20.000)よりも前であるため、条件はtrueとなり、304 Not Modifiedが正しく返されるようになります。
この変更は、HTTPヘッダーの秒単位の精度とファイルシステムの秒以下の精度の間の「曖昧さ」を許容し、より堅牢なキャッシュ検証を実現します。これにより、サーバーは、クライアントがIf-Modified-Sinceで指定した時刻と同じ秒内に更新されたリソースに対しても、正しく304 Not Modifiedを返すことができるようになります。
コアとなるコードの変更箇所
src/pkg/net/http/fs.goファイルのcheckLastModified関数内の以下の行が変更されました。
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -186,7 +186,10 @@ func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
if modtime.IsZero() {
return false
}
- if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.After(t) {
+
+ // The Date-Modified header truncates sub-second precision, so
+ // use mtime < t+1s instead of mtime <= t to check for unmodified.
+ if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
w.WriteHeader(StatusNotModified)
return true
}
コアとなるコードの解説
変更された行は、checkLastModified関数内でIf-Modified-Sinceヘッダーの値を処理する部分です。
元のコード:
if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.After(t) {
この行では、If-Modified-Sinceヘッダーの値が正常にtime.Time型のtにパースされ、かつファイルの最終更新時刻modtimeがパースされた時刻tよりも「後」である場合に、304 Not Modifiedを返さない(つまり、リソースが変更されたと判断する)というロジックでした。modtime.After(t)はmodtime > tと同じ意味です。したがって、modtime <= tの場合に304 Not Modifiedを返すという意図でした。
変更後のコード:
// The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
この変更では、比較ロジックがmodtime.Before(t.Add(1*time.Second))に変わりました。
t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")): クライアントから送られてきたIf-Modified-Sinceヘッダーの文字列を、標準のHTTP日付フォーマット(TimeFormat)に従ってtime.Time型のtにパースします。エラーが発生した場合は、この条件は満たされません。modtime.Before(t.Add(1*time.Second)): これが新しい比較条件です。t.Add(1*time.Second): パースされた時刻tに1秒を加算します。例えば、tが2012-02-12 23:45:19.000であれば、これは2012-02-12 23:45:20.000になります。modtime.Before(...): ファイルの最終更新時刻modtimeが、tに1秒を加算した時刻よりも厳密に前であるかどうかをチェックします。
この新しいロジックにより、modtimeがtと同じ秒内(例えばtが23:45:19.000でmodtimeが23:45:19.500)である場合でも、modtimeはt.Add(1*time.Second)(23:45:20.000)よりも前であるため、条件はtrueとなり、w.WriteHeader(StatusNotModified)が実行され、304 Not Modifiedステータスがクライアントに返されます。これにより、HTTPヘッダーの秒単位の精度とファイルシステムの秒以下の精度の間の不一致が適切に処理され、より正確なキャッシュ検証が可能になります。
関連リンク
- RFC 7232 - Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests:
- https://datatracker.ietf.org/doc/html/rfc7232#section-2.2 (If-Modified-Since ヘッダーについて)
- https://datatracker.ietf.org/doc/html/rfc7232#section-2.2 (Last-Modified ヘッダーについて)
- RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content:
- https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1 (Date/Time Formatsについて)
- Go言語
timeパッケージのドキュメント:
参考にした情報源リンク
- https://github.com/golang/go/commit/c58b6ad02203cc0f4ba3cd0a38ce222d177cc75e
- https://golang.org/cl/5655052 (Go Code Review)
- HTTP/1.1 RFCs (RFC 7231, RFC 7232)
- Go言語の
timeパッケージの公式ドキュメント