[インデックス 13782] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内の ServeContent
関数に、HTTPの条件付きリクエストヘッダである If-None-Match
および If-Range
のサポートを追加するものです。これにより、ウェブサーバーがクライアントからのキャッシュ検証リクエストや部分コンテンツリクエストに、より効率的かつ標準に準拠した形で応答できるようになります。
コミット
commit a7743d7ad25a5a37699c1bc8378f8f25239596f72
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Sep 10 10:16:09 2012 -0700
net/http: add If-None-Match and If-Range support to ServeContent
Also, clear Content-Type and Content-Length on Not Modified
responses before server.go strips them and spams the logs with
warnings.
R=rsc
CC=golang-dev
https://golang.org/cl/6503090
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a7743d7ad25a5a37699c1bc8378f8f2523956f72
元コミット内容
net/http: add If-None-Match and If-Range support to ServeContent
このコミットは、net/http
パッケージの ServeContent
関数に If-None-Match
および If-Range
ヘッダのサポートを追加します。
また、Not Modified
(304) レスポンスにおいて、server.go
が Content-Type
と Content-Length
を削除して警告ログを出す前に、これらのヘッダをクリアするように修正します。
変更の背景
HTTP/1.1では、クライアントとサーバー間の通信効率を向上させるために、キャッシュメカニズムが導入されています。その中でも、条件付きリクエストは非常に重要な役割を果たします。
- キャッシュの効率化: クライアントが以前に取得したリソースのコピーを持っている場合、サーバーにそのリソースが変更されていないかを確認するだけで済みます。これにより、不要なデータ転送を削減し、ネットワーク帯域幅の消費を抑え、応答時間を短縮できます。
- 部分コンテンツの取得: 大容量のファイルを扱う場合、クライアントはファイル全体を再ダウンロードするのではなく、中断されたダウンロードを再開したり、特定の範囲のデータのみを取得したりしたい場合があります。
Range
リクエストとIf-Range
ヘッダはそのためのメカニズムを提供します。 - 既存の課題: 以前の
ServeContent
関数は、If-Modified-Since
ヘッダによるLast-Modified
ベースのキャッシュ検証はサポートしていましたが、より強力なキャッシュ検証メカニズムであるETag
を利用したIf-None-Match
や、部分コンテンツリクエストとキャッシュ検証を組み合わせるIf-Range
には対応していませんでした。この不足は、より堅牢で効率的なウェブサービスを構築する上で課題となっていました。 - ログの警告:
Not Modified
(304) レスポンスはボディを持たないため、Content-Type
やContent-Length
ヘッダは通常含まれません。しかし、Goのnet/http
内部でこれらのヘッダが設定されたまま304
レスポンスが返されると、server.go
がこれらを削除し、その際に警告ログが出力されるという問題がありました。このコミットは、この不必要な警告を解消することも目的としています。
このコミットは、これらの課題を解決し、net/http
パッケージがより標準に準拠し、効率的なHTTPキャッシュおよび部分コンテンツ配信をサポートできるようにするために行われました。
前提知識の解説
このコミットを理解するためには、以下のHTTPヘッダと概念についての知識が必要です。
HTTPキャッシュと条件付きリクエスト
HTTPキャッシュは、ウェブのパフォーマンスを向上させるための重要なメカニズムです。クライアント(ブラウザなど)は、以前に取得したリソースのコピーをローカルに保存し、次回同じリソースが必要になったときに、サーバーに問い合わせることなくキャッシュから提供できる場合があります。しかし、キャッシュされたリソースが古くなっていないか(Staleになっていないか)を確認する必要があります。この確認のために「条件付きリクエスト」が使用されます。
ETag (Entity Tag)
ETag
は、リソースの特定のバージョンを識別するための不透明な識別子です。サーバーはリソースの ETag
を生成し、ETag
ヘッダとしてレスポンスに含めます。リソースが変更されると、その ETag
も変更されます。
例: ETag: "abcdef123456"
If-None-Match
クライアントがキャッシュされたリソースの ETag
を持っている場合、If-None-Match
リクエストヘッダにその ETag
を含めてサーバーに送信します。
- サーバーの動作:
- もしリクエストされたリソースの現在の
ETag
がIf-None-Match
ヘッダの値と一致する場合、サーバーはリソースが変更されていないと判断し、304 Not Modified
ステータスコードを返します。この場合、レスポンスボディは空であり、クライアントは自身のキャッシュを使用します。 - もし
ETag
が一致しない場合、サーバーはリソースが変更されたと判断し、200 OK
ステータスコードと共に新しいリソースのコンテンツを返します。
- もしリクエストされたリソースの現在の
If-None-Match: *
は、リソースが既に存在する場合にリクエストを処理しないことを意味します。これは、PUTリクエストなどでリソースの重複作成を防ぐためによく使用されます。
Last-Modified と If-Modified-Since
これらは ETag
と同様にキャッシュ検証に使用されますが、時間ベースです。
- Last-Modified: サーバーがリソースの最終更新日時を
Last-Modified
ヘッダとしてレスポンスに含めます。 - If-Modified-Since: クライアントがキャッシュされたリソースの
Last-Modified
日時をIf-Modified-Since
リクエストヘッダに含めてサーバーに送信します。 - サーバーの動作:
- もしリソースが
If-Modified-Since
で指定された日時以降に変更されていない場合、サーバーは304 Not Modified
を返します。 - もしリソースが変更されている場合、サーバーは
200 OK
と共に新しいリソースを返します。
- もしリソースが
If-Range
If-Range
ヘッダは、Range
リクエストと組み合わせて使用されます。クライアントがリソースの部分的なコンテンツを要求する際に、そのリソースのキャッシュされたバージョンがまだ有効であるかどうかを確認するために使用されます。
If-Range
の値は、ETag
またはLast-Modified
日時のいずれかです。- サーバーの動作:
- もし
If-Range
の値がリソースの現在のETag
またはLast-Modified
と一致する場合、サーバーはRange
ヘッダを尊重し、206 Partial Content
ステータスコードと共に指定された範囲のコンテンツを返します。 - もし
If-Range
の値が一致しない場合(つまり、リソースが変更されている場合)、サーバーはRange
ヘッダを無視し、200 OK
ステータスコードと共にリソース全体を返します。
- もし
これにより、クライアントはリソースが変更されていない場合は部分的なコンテンツを効率的に取得でき、変更されている場合は安全にリソース全体を再取得できます。
Go言語の net/http
パッケージ
Goの net/http
パッケージは、HTTPクライアントとサーバーを実装するための基本的な機能を提供します。
http.ResponseWriter
: HTTPレスポンスを構築するためのインターフェース。ヘッダの設定やボディの書き込みを行います。http.Request
: 受信したHTTPリクエストを表す構造体。ヘッダ、メソッド、URLなどの情報を含みます。http.ServeContent
:io.ReadSeeker
インターフェースを実装するコンテンツ(ファイルなど)をHTTPレスポンスとして提供するためのユーティリティ関数。Range
リクエストの処理やLast-Modified
ベースのキャッシュ検証を自動的に行います。
技術的詳細
このコミットは、主に src/pkg/net/http/fs.go
ファイル内の ServeContent
関数とその関連ヘルパー関数に変更を加えています。
ServeContent
関数の変更
ServeContent
関数は、コンテンツの提供ロジックをカプセル化しています。このコミットでは、既存の checkLastModified
に加えて、新たに checkETag
関数を呼び出すように変更されました。
// src/pkg/net/http/fs.go
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, content io.ReadSeeker) (size int64, err error) {
// ...
if checkLastModified(w, r, modtime) {
return
}
rangeReq, done := checkETag(w, r) // 新たに追加
if done {
return
}
// ...
if size >= 0 {
ranges, err := parseRange(r.Header.Get("Range"), size) // 変更前
ranges, err := parseRange(rangeReq, size) // 変更後: checkETagの結果を使用
// ...
}
// ...
}
checkETag
が返す rangeReq
は、If-Range
ヘッダの検証結果に基づいて、実際に処理すべき Range
ヘッダの値を示します。If-Range
が一致しない場合、rangeReq
は空文字列となり、parseRange
は Range
リクエストを無視してコンテンツ全体を返すように動作します。
checkLastModified
関数の変更
checkLastModified
関数は、If-Modified-Since
ヘッダを処理し、リソースが変更されていない場合に 304 Not Modified
レスポンスを返します。このコミットでは、304
レスポンスを返す直前に Content-Type
と Content-Length
ヘッダを明示的に削除する処理が追加されました。
// src/pkg/net/http/fs.go
func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
// ...
if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
h := w.Header()
delete(h, "Content-Type") // 新たに追加
delete(h, "Content-Length") // 新たに追加
w.WriteHeader(StatusNotModified)
return true
}
return false
}
これは、304 Not Modified
レスポンスにはボディが含まれないため、これらのヘッダが存在すると不適切であり、Goの内部ロジックで警告がログに出力されるのを防ぐための修正です。
checkETag
関数の追加
このコミットの主要な変更点であり、If-None-Match
と If-Range
ヘッダの処理ロジックを実装する新しいヘルパー関数 checkETag
が追加されました。
// src/pkg/net/http/fs.go
func checkETag(w ResponseWriter, r *Request) (rangeReq string, done bool) {
etag := w.Header().Get("Etag") // レスポンスヘッダからETagを取得
rangeReq = r.Header().Get("Range") // リクエストヘッダからRangeを取得
// If-Range ヘッダの処理
if ir := r.Header().Get("If-Range"); ir != "" && ir != etag {
// If-Range の値が現在のETagと一致しない場合、Rangeリクエストを無効化
rangeReq = ""
}
// If-None-Match ヘッダの処理
if inm := r.Header().Get("If-None-Match"); inm != "" {
if etag == "" {
// ETagが設定されていない場合は処理を続行
return rangeReq, false
}
// GET/HEAD リクエストのみをサポート (TODO: 他のメソッドへの対応)
if r.Method != "GET" && r.Method != "HEAD" {
return rangeReq, false
}
// If-None-Match の値がETagと一致するか、または "*" の場合
if inm == etag || inm == "*" {
h := w.Header()
delete(h, "Content-Type") // Content-Typeをクリア
delete(h, "Content-Length") // Content-Lengthをクリア
w.WriteHeader(StatusNotModified) // 304 Not Modifiedを返す
return "", true // リクエスト処理完了
}
}
return rangeReq, false // 処理続行
}
この関数は以下のロジックを実行します。
- ETagの取得:
ResponseWriter
に既に設定されているETag
ヘッダの値を取得します。ServeContent
を呼び出す前に、呼び出し元がETag
を設定していることを前提としています。 - If-Rangeの処理:
- リクエストに
If-Range
ヘッダが存在し、その値が現在のリソースのETag
と一致しない場合、Range
リクエストは無効と判断され、rangeReq
が空文字列に設定されます。これにより、後続のparseRange
関数はRange
ヘッダを無視し、リソース全体を返すようになります。 TODO
コメントで、Last-Modified
ベースのIf-Range
のサポートが将来の課題として挙げられています。
- リクエストに
- If-None-Matchの処理:
- リクエストに
If-None-Match
ヘッダが存在する場合に処理を行います。 ETag
が設定されていない場合は、キャッシュ検証ができないため処理を続行します。- 現在のところ、
GET
およびHEAD
メソッドのリクエストのみをサポートしています。TODO
コメントで、他のメソッド(例:PUT
)に対するIf-None-Match
の処理(異なるステータスコードの送信や、弱いキャッシュバリデータ(W/
プレフィックス)の扱い)が将来の課題として挙げられています。 If-None-Match
の値が現在のETag
と一致するか、または*
(全てのリソースにマッチ) である場合、リソースは変更されていないと判断されます。この場合、Content-Type
とContent-Length
ヘッダをクリアし、304 Not Modified
ステータスコードを返して処理を終了します。TODO
コメントで、カンマ区切りのIf-None-Match
値のリストへの対応が将来の課題として挙げられています。
- リクエストに
テストファイルの変更 (src/pkg/net/http/fs_test.go
)
fs_test.go
ファイルは、ServeContent
の新しい機能を検証するために大幅に拡張されました。
testCase
構造体の導入: 複数のテストシナリオを簡潔に定義できるように、testCase
という新しい構造体が導入されました。これにより、テストの可読性と保守性が向上しています。- 網羅的なテストケース:
Last-Modified
がない場合、ある場合。If-Modified-Since
による304 Not Modified
レスポンスの検証。特に、Content-Type
が明示的に設定されている場合でも304
レスポンスでクリアされることを確認するテストが追加されています。If-None-Match
とETag
を使用した304 Not Modified
レスポンスの検証。Range
リクエストが正しく処理されるかどうかの検証。If-Range
ヘッダが存在し、その値が現在のETag
と一致しない場合に、Range
リクエストが無視され、コンテンツ全体が返されることを確認するテスト (range_no_match
) が追加されています。
これらのテストケースは、ServeContent
が If-None-Match
と If-Range
ヘッダを正しく処理し、期待されるHTTPステータスコードとヘッダを返すことを保証します。
コアとなるコードの変更箇所
src/pkg/net/http/fs.go
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -100,6 +100,9 @@ func dirList(w ResponseWriter, f File) {
// The content's Seek method must work: ServeContent uses
// a seek to the end of the content to determine its size.
//
+// If the caller has set w's ETag header, ServeContent uses it to
+// handle requests using If-Range and If-None-Match.
+//
// Note that *os.File implements the io.ReadSeeker interface.
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
size, err := content.Seek(0, os.SEEK_END)
@@ -122,6 +125,10 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
if checkLastModified(w, r, modtime) {
return
}
+ rangeReq, done := checkETag(w, r)
+ if done {
+ return
+ }
code := StatusOK
@@ -148,7 +155,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
sendSize := size
var sendContent io.Reader = content
if size >= 0 {
- ranges, err := parseRange(r.Header.Get("Range"), size)
+ ranges, err := parseRange(rangeReq, size)
if err != nil {
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
return
@@ -240,6 +247,9 @@ func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
// 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)) {
+ h := w.Header()
+ delete(h, "Content-Type")
+ delete(h, "Content-Length")
w.WriteHeader(StatusNotModified)
return true
}
@@ -247,6 +257,58 @@ func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
return false
}
+// checkETag implements If-None-Match and If-Range checks.
+// The ETag must have been previously set in the ResponseWriter's headers.
+//
+// The return value is the effective request "Range" header to use and
+// whether this request is now considered done.
+func checkETag(w ResponseWriter, r *Request) (rangeReq string, done bool) {
+ etag := w.Header().Get("Etag")
+ rangeReq = r.Header().Get("Range")
+
+ // Invalidate the range request if the entity doesn't match the one
+ // the client was expecting.
+ // "If-Range: version" means "ignore the Range: header unless version matches the
+ // current file."
+ // We only support ETag versions.
+ // The caller must have set the ETag on the response already.
+ if ir := r.Header().Get("If-Range"); ir != "" && ir != etag {
+ // TODO(bradfitz): handle If-Range requests with Last-Modified
+ // times instead of ETags? I'd rather not, at least for
+ // now. That seems like a bug/compromise in the RFC 2616, and
+ // I've never heard of anybody caring about that (yet).
+ rangeReq = ""
+ }
+
+ if inm := r.Header().Get("If-None-Match"); inm != "" {
+ // Must know ETag.
+ if etag == "" {
+ return rangeReq, false
+ }
+
+ // TODO(bradfitz): non-GET/HEAD requests require more work:
+ // sending a different status code on matches, and
+ // also can't use weak cache validators (those with a "W/"
+ // prefix). But most users of ServeContent will be using
+ // it on GET or HEAD, so only support those for now.
+ if r.Method != "GET" && r.Method != "HEAD" {
+ return rangeReq, false
+ }
+
+ // TODO(bradfitz): deal with comma-separated or multiple-valued
+ // list of If-None-match values. For now just handle the common
+ // case of a single item.
+ if inm == etag || inm == "*" {
+ h := w.Header()
+ delete(h, "Content-Type")
+ delete(h, "Content-Length")
+ w.WriteHeader(StatusNotModified)
+ return "", true
+ }
+ }
+ return rangeReq, false
+}
+
// name is '/'-separated, not filepath.Separator.
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
const indexPage = "/index.html"
src/pkg/net/http/fs_test.go
テストファイルは大幅に変更されており、新しいテストケースの追加と既存のテストの構造化が行われています。変更量が多いため、ここでは差分全体は掲載しませんが、主な変更点は以下の通りです。
TestServeContent
関数がtestCase
構造体とループを使用して、複数のシナリオをテストするように書き換えられました。mustStat
ヘルパー関数が追加されました。no_last_modified
,with_last_modified
,not_modified_modtime
,not_modified_modtime_with_contenttype
,not_modified_etag
,range_good
,range_no_match
などの新しいテストケースが追加されました。
コアとなるコードの解説
ServeContent
の変更
ServeContent
関数は、HTTPリクエストに対するコンテンツの提供を抽象化します。このコミットでは、既存の Last-Modified
ベースのキャッシュ検証 (checkLastModified
) に加えて、ETag
ベースのキャッシュ検証 (checkETag
) が導入されました。
rangeReq, done := checkETag(w, r)
の行が追加され、checkETag
の結果に基づいて、リクエストの処理を続行するか (done
が false
)、または 304 Not Modified
を返して終了するか (done
が true
) が決定されます。また、parseRange
関数に渡される Range
ヘッダの値が、checkETag
が返した rangeReq
に変更されました。これにより、If-Range
ヘッダの条件が満たされない場合に Range
リクエストが自動的に無効化されるようになります。
checkLastModified
の変更
checkLastModified
関数は、If-Modified-Since
ヘッダを処理し、リソースが変更されていない場合に 304 Not Modified
ステータスを返します。このコミットでは、304
レスポンスを書き込む直前に、Content-Type
と Content-Length
ヘッダをレスポンスヘッダから削除する処理が追加されました。これは、304
レスポンスにはボディが含まれないため、これらのヘッダが存在すると不適切であり、Goの内部で不必要な警告ログが出力されるのを防ぐためのものです。
checkETag
関数の追加
この関数は、If-None-Match
と If-Range
ヘッダの処理ロジックをカプセル化します。
ETag
の取得:w.Header().Get("Etag")
を通じて、レスポンスに既に設定されているETag
を取得します。これは、ServeContent
を呼び出す前に、呼び出し元が適切なETag
を設定していることを前提としています。If-Range
の処理:- リクエストに
If-Range
ヘッダがあり、その値が現在のリソースのETag
と一致しない場合、これはリソースがクライアントのキャッシュ以降に変更されたことを意味します。この場合、Range
リクエストは無効と見なされ、rangeReq
が空文字列に設定されます。これにより、クライアントはリソース全体を再取得することになります。 TODO
コメントは、Last-Modified
ベースのIf-Range
のサポートが将来の課題であることを示しています。
- リクエストに
If-None-Match
の処理:- リクエストに
If-None-Match
ヘッダがある場合、サーバーはクライアントが持っているETag
と現在のリソースのETag
を比較します。 ETag
が設定されていない場合、キャッシュ検証は行われません。- 現在の実装では、
GET
およびHEAD
メソッドのリクエストのみをサポートしています。他のメソッド(例:PUT
)に対するIf-None-Match
の処理はより複雑であり、将来の課題として挙げられています。 - もし
If-None-Match
の値が現在のETag
と一致するか、または*
(ワイルドカード) である場合、リソースは変更されていないと判断されます。この場合、Content-Type
とContent-Length
ヘッダをクリアし、304 Not Modified
ステータスコードを返します。done
がtrue
に設定されることで、ServeContent
はこれ以上の処理を行わずに終了します。 TODO
コメントは、カンマ区切りのIf-None-Match
値のリストへの対応が将来の課題であることを示しています。
- リクエストに
この checkETag
関数は、HTTPのキャッシュ検証と部分コンテンツ配信の標準を net/http
パッケージに組み込むための重要なステップです。
関連リンク
- HTTP/1.1: Conditional Requests - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24 (RFC 2616 - If-None-Match)
- HTTP/1.1: Range Requests - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 (RFC 2616 - If-Range)
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go
net/http/fs.go
source code: https://github.com/golang/go/blob/master/src/net/http/fs.go
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616.html
- MDN Web Docs - HTTP caching: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
- MDN Web Docs - ETag: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
- MDN Web Docs - If-None-Match: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
- MDN Web Docs - If-Range: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range
- MDN Web Docs - Last-Modified: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
- MDN Web Docs - If-Modified-Since: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
- Go
net/http
ServeContent documentation: https://pkg.go.dev/net/http#ServeContent