Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.goContent-TypeContent-Length を削除して警告ログを出す前に、これらのヘッダをクリアするように修正します。

変更の背景

HTTP/1.1では、クライアントとサーバー間の通信効率を向上させるために、キャッシュメカニズムが導入されています。その中でも、条件付きリクエストは非常に重要な役割を果たします。

  • キャッシュの効率化: クライアントが以前に取得したリソースのコピーを持っている場合、サーバーにそのリソースが変更されていないかを確認するだけで済みます。これにより、不要なデータ転送を削減し、ネットワーク帯域幅の消費を抑え、応答時間を短縮できます。
  • 部分コンテンツの取得: 大容量のファイルを扱う場合、クライアントはファイル全体を再ダウンロードするのではなく、中断されたダウンロードを再開したり、特定の範囲のデータのみを取得したりしたい場合があります。Range リクエストと If-Range ヘッダはそのためのメカニズムを提供します。
  • 既存の課題: 以前の ServeContent 関数は、If-Modified-Since ヘッダによる Last-Modified ベースのキャッシュ検証はサポートしていましたが、より強力なキャッシュ検証メカニズムである ETag を利用した If-None-Match や、部分コンテンツリクエストとキャッシュ検証を組み合わせる If-Range には対応していませんでした。この不足は、より堅牢で効率的なウェブサービスを構築する上で課題となっていました。
  • ログの警告: Not Modified (304) レスポンスはボディを持たないため、Content-TypeContent-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 を含めてサーバーに送信します。

  • サーバーの動作:
    • もしリクエストされたリソースの現在の ETagIf-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 は空文字列となり、parseRangeRange リクエストを無視してコンテンツ全体を返すように動作します。

checkLastModified 関数の変更

checkLastModified 関数は、If-Modified-Since ヘッダを処理し、リソースが変更されていない場合に 304 Not Modified レスポンスを返します。このコミットでは、304 レスポンスを返す直前に Content-TypeContent-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-MatchIf-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 // 処理続行
 }

この関数は以下のロジックを実行します。

  1. ETagの取得: ResponseWriter に既に設定されている ETag ヘッダの値を取得します。ServeContent を呼び出す前に、呼び出し元が ETag を設定していることを前提としています。
  2. If-Rangeの処理:
    • リクエストに If-Range ヘッダが存在し、その値が現在のリソースの ETag と一致しない場合、Range リクエストは無効と判断され、rangeReq が空文字列に設定されます。これにより、後続の parseRange 関数は Range ヘッダを無視し、リソース全体を返すようになります。
    • TODO コメントで、Last-Modified ベースの If-Range のサポートが将来の課題として挙げられています。
  3. If-None-Matchの処理:
    • リクエストに If-None-Match ヘッダが存在する場合に処理を行います。
    • ETag が設定されていない場合は、キャッシュ検証ができないため処理を続行します。
    • 現在のところ、GET および HEAD メソッドのリクエストのみをサポートしています。TODO コメントで、他のメソッド(例: PUT)に対する If-None-Match の処理(異なるステータスコードの送信や、弱いキャッシュバリデータ(W/ プレフィックス)の扱い)が将来の課題として挙げられています。
    • If-None-Match の値が現在の ETag と一致するか、または * (全てのリソースにマッチ) である場合、リソースは変更されていないと判断されます。この場合、Content-TypeContent-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-MatchETag を使用した 304 Not Modified レスポンスの検証。
    • Range リクエストが正しく処理されるかどうかの検証。
    • If-Range ヘッダが存在し、その値が現在の ETag と一致しない場合に、Range リクエストが無視され、コンテンツ全体が返されることを確認するテスト (range_no_match) が追加されています。

これらのテストケースは、ServeContentIf-None-MatchIf-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 の結果に基づいて、リクエストの処理を続行するか (donefalse)、または 304 Not Modified を返して終了するか (donetrue) が決定されます。また、parseRange 関数に渡される Range ヘッダの値が、checkETag が返した rangeReq に変更されました。これにより、If-Range ヘッダの条件が満たされない場合に Range リクエストが自動的に無効化されるようになります。

checkLastModified の変更

checkLastModified 関数は、If-Modified-Since ヘッダを処理し、リソースが変更されていない場合に 304 Not Modified ステータスを返します。このコミットでは、304 レスポンスを書き込む直前に、Content-TypeContent-Length ヘッダをレスポンスヘッダから削除する処理が追加されました。これは、304 レスポンスにはボディが含まれないため、これらのヘッダが存在すると不適切であり、Goの内部で不必要な警告ログが出力されるのを防ぐためのものです。

checkETag 関数の追加

この関数は、If-None-MatchIf-Range ヘッダの処理ロジックをカプセル化します。

  1. ETag の取得: w.Header().Get("Etag") を通じて、レスポンスに既に設定されている ETag を取得します。これは、ServeContent を呼び出す前に、呼び出し元が適切な ETag を設定していることを前提としています。
  2. If-Range の処理:
    • リクエストに If-Range ヘッダがあり、その値が現在のリソースの ETag と一致しない場合、これはリソースがクライアントのキャッシュ以降に変更されたことを意味します。この場合、Range リクエストは無効と見なされ、rangeReq が空文字列に設定されます。これにより、クライアントはリソース全体を再取得することになります。
    • TODO コメントは、Last-Modified ベースの If-Range のサポートが将来の課題であることを示しています。
  3. If-None-Match の処理:
    • リクエストに If-None-Match ヘッダがある場合、サーバーはクライアントが持っている ETag と現在のリソースの ETag を比較します。
    • ETag が設定されていない場合、キャッシュ検証は行われません。
    • 現在の実装では、GET および HEAD メソッドのリクエストのみをサポートしています。他のメソッド(例: PUT)に対する If-None-Match の処理はより複雑であり、将来の課題として挙げられています。
    • もし If-None-Match の値が現在の ETag と一致するか、または * (ワイルドカード) である場合、リソースは変更されていないと判断されます。この場合、Content-TypeContent-Length ヘッダをクリアし、304 Not Modified ステータスコードを返します。donetrue に設定されることで、ServeContent はこれ以上の処理を行わずに終了します。
    • TODO コメントは、カンマ区切りの If-None-Match 値のリストへの対応が将来の課題であることを示しています。

この checkETag 関数は、HTTPのキャッシュ検証と部分コンテンツ配信の標準を net/http パッケージに組み込むための重要なステップです。

関連リンク

参考にした情報源リンク