[インデックス 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
パッケージの公式ドキュメント