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

[インデックス 16721] ファイルの概要

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける ServeContent 関数の振る舞いを改善するものです。具体的には、HTTPレスポンスを生成する際に、コンテンツのサイズを決定するための Seek 操作を、本当に必要になるまで遅延させるように変更しています。これにより、特に 304 Not Modified レスポンスを返す場合に、不要なI/O操作を削減し、効率性と堅牢性を向上させています。

コミット

commit d178c016c2b6ae2403986c730d8d11ed95a8211f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jul 10 13:29:52 2013 +1000

    net/http: in ServeContent, don't seek on content until necessary
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/11080043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d178c016c2b6ae2403986c730d8d11ed95a8211f

元コミット内容

net/http: in ServeContent, don't seek on content until necessary

このコミットは、net/http パッケージの ServeContent 関数において、コンテンツの Seek 操作を必要になるまで行わないように変更します。

変更の背景

net/http.ServeContent 関数は、HTTPリクエストに対して io.ReadSeeker インターフェースを実装するコンテンツ(例えばファイル)を効率的に提供するために設計されています。この関数は、コンテンツのサイズを把握するために、提供する io.ReadSeekerSeek メソッドを呼び出し、コンテンツの終端に移動してから先頭に戻るという操作を初期段階で行っていました。

しかし、HTTPプロトコルにはキャッシュ機構があり、クライアントが If-Modified-SinceIf-None-Match といったヘッダーを送信することで、サーバーはコンテンツが変更されていない場合に 304 Not Modified ステータスコードを返すことができます。この場合、サーバーは実際のコンテンツを送信する必要がなく、したがってコンテンツのサイズを事前に知る必要もありません。

変更前の実装では、304 Not Modified レスポンスを返すことが可能な状況であっても、無条件に Seek 操作を実行していました。これは、特に Seek 操作がコストの高いI/O操作である場合(例えば、ネットワーク越しに提供されるコンテンツや、仮想的なコンテンツの場合)、無駄なリソース消費やパフォーマンスの低下を招く可能性がありました。また、Seek 操作をサポートしない io.ReadSeeker の実装に対しては、不必要なエラーを引き起こす可能性もありました。

このコミットの目的は、このような不要な Seek 操作を排除し、ServeContent の効率性と汎用性を向上させることにあります。

前提知識の解説

1. io.Reader, io.Seeker, io.ReadSeeker インターフェース

Go言語の io パッケージは、I/O操作のための基本的なインターフェースを提供します。

  • io.Reader: データを読み込むための最も基本的なインターフェースです。Read(p []byte) (n int, err error) メソッドを持ち、バイトスライス p にデータを読み込み、読み込んだバイト数 n とエラーを返します。
  • io.Seeker: データの読み書き位置(オフセット)を変更するためのインターフェースです。Seek(offset int64, whence int) (int64, error) メソッドを持ちます。
    • offset: シークするバイト数。
    • whence: シークの基準位置を指定します。os.SEEK_SET(ファイルの先頭から)、os.SEEK_CUR(現在の位置から)、os.SEEK_END(ファイルの終端から)のいずれかです。
    • 戻り値は、新しいオフセットとエラーです。
  • io.ReadSeeker: io.Readerio.Seeker の両方の機能を兼ね備えたインターフェースです。ファイルのように、読み込みとシークの両方が可能なデータソースを表します。

2. HTTPキャッシュと 304 Not Modified

HTTPプロトコルは、クライアントとサーバー間の通信を効率化するためにキャッシュ機構を提供します。

  • Last-Modified ヘッダー: サーバーがリソースの最終更新日時をクライアントに伝えます。
  • ETag ヘッダー: サーバーがリソースの特定のバージョンを識別するための不透明なエンティティタグ(通常はハッシュ値など)をクライアントに伝えます。
  • If-Modified-Since リクエストヘッダー: クライアントが、指定された日時以降にリソースが変更された場合にのみリソースを要求します。
  • If-None-Match リクエストヘッダー: クライアントが、指定された ETag と一致しない場合にのみリソースを要求します。

サーバーはこれらのリクエストヘッダーを受け取ると、リソースが変更されていないことを確認した場合、304 Not Modified ステータスコードを返します。この場合、レスポンスボディは空であり、クライアントは自身のキャッシュされたコピーを使用します。これにより、ネットワーク帯域幅の節約とレスポンス時間の短縮が実現されます。

3. net/http.ServeContent 関数

net/http.ServeContent は、io.ReadSeeker インターフェースを実装するコンテンツをHTTPレスポンスとして提供するためのユーティリティ関数です。この関数は、コンテンツのMIMEタイプ検出、Last-ModifiedETag ヘッダーの処理、レンジリクエスト(Content-Range ヘッダー)の処理など、HTTPコンテンツ配信に必要な多くの機能を提供します。

技術的詳細

変更の核心は、ServeContent 関数がコンテンツのサイズを取得する方法にあります。

変更前: ServeContent 関数は、受け取った io.ReadSeeker (content) に対して、まず content.Seek(0, os.SEEK_END) を呼び出してコンテンツの終端に移動し、その戻り値からコンテンツの全長 (size) を取得していました。その後、content.Seek(0, os.SEEK_SET) を呼び出してコンテンツの先頭に戻していました。これらの操作は、ServeContent の処理の非常に早い段階で、checkLastModified などのキャッシュ関連のチェックが行われる前に行われていました。

// 変更前のServeContentの抜粋
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
    size, err := content.Seek(0, os.SEEK_END) // ここでSeekが実行される
    if err != nil { /* ... */ }
    _, err = content.Seek(0, os.SEEK_SET)    // ここでもSeekが実行される
    if err != nil { /* ... */ }
    serveContent(w, req, name, modtime, size, content)
}

変更後: このコミットでは、コンテンツのサイズを取得するロジックを sizeFunc func() (int64, error) という関数型に抽象化し、これを ServeContent および内部で呼び出される serveContent 関数に渡すようにしました。

ServeContent 関数は、sizeFunc を定義する際に、以前の Seek 操作をこの関数内にカプセル化します。

// 変更後のServeContentの抜粋
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
    sizeFunc := func() (int64, error) {
        size, err := content.Seek(0, os.SEEK_END) // Seek操作はここに含まれる
        if err != nil { return 0, errSeeker }
        _, err = content.Seek(0, os.SEEK_SET)
        if err != nil { return 0, errSeeker }
        return size, nil
    }
    serveContent(w, req, name, modtime, sizeFunc, content)
}

そして、serveContent 関数内で、checkLastModified が呼び出された後に初めて sizeFunc() が呼び出されるように変更されました。

// 変更後のserveContentの抜粋
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
    if checkLastModified(w, r, modtime) {
        return // 304 Not Modifiedの場合、ここで処理が終了し、sizeFuncは呼び出されない
    }
    // ...
    size, err := sizeFunc() // 304でない場合のみ、ここでsizeFuncが呼び出され、Seekが実行される
    if err != nil { /* ... */ }
    // ...
}

この変更により、checkLastModifiedtrue を返し、304 Not Modified レスポンスが送信される場合、sizeFunc は決して呼び出されず、結果として content に対する Seek 操作も実行されません。これにより、不要なI/O操作が回避され、パフォーマンスが向上し、Seek 操作がコスト高またはサポートされていない io.ReadSeeker の実装に対してもより堅牢になります。

また、errSeeker という新しい内部エラー変数が導入されました。これは、sizeFunc 内で Seek 操作が失敗した場合に返されるエラーです。このエラーは、元の Seek エラーの詳細をHTTPレスポンスに含めないようにするために使用されます。これにより、内部的なエラーメッセージが外部に漏洩するのを防ぎ、セキュリティと情報隠蔽の観点からも改善されています。

テストコード (fs_test.go) では、panicOnSeek というダミーの io.ReadSeeker 実装が追加されました。これは Seek メソッドが呼び出されるとパニックを発生させるものです。この panicOnSeek を使用したテストケース (not_modified_etag_no_seek) を追加することで、304 Not Modified のシナリオで実際に Seek 操作が行われないことを検証しています。

コアとなるコードの変更箇所

src/pkg/net/http/fs.go

--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -105,23 +105,31 @@ func dirList(w ResponseWriter, f File) {
 //
 // 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)
-	if err != nil {
-		Error(w, "seeker can't seek", StatusInternalServerError)
-		return
-	}
-	_, err = content.Seek(0, os.SEEK_SET)
-	if err != nil {
-		Error(w, "seeker can't seek", StatusInternalServerError)
-		return
+	sizeFunc := func() (int64, error) {
+		size, err := content.Seek(0, os.SEEK_END)
+		if err != nil {
+			return 0, errSeeker
+		}
+		_, err = content.Seek(0, os.SEEK_SET)
+		if err != nil {
+			return 0, errSeeker
+		}
+		return size, nil
 	}
-	serveContent(w, req, name, modtime, size, content)
+	serveContent(w, req, name, modtime, sizeFunc, content)
 }
 
+// errSeeker is returned by ServeContent's sizeFunc when the content
+// doesn't seek properly. The underlying Seeker's error text isn't
+// included in the sizeFunc reply so it's not sent over HTTP to end
+// users.
+var errSeeker = errors.New("seeker can't seek")
+
 // if name is empty, filename is unknown. (used for mime type, before sniffing)
 // if modtime.IsZero(), modtime is unknown.
 // content must be seeked to the beginning of the file.
-func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
+// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
+func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
 	if checkLastModified(w, r, modtime) {
 		return
 	}
@@ -151,6 +159,12 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
 		w.Header().Set("Content-Type", ctype)
 	}
 
+	size, err := sizeFunc()
+	if err != nil {
+		Error(w, err.Error(), StatusInternalServerError)
+		return
+	}
+
 	// handle Content-Range header.
 	sendSize := size
 	var sendContent io.Reader = content
@@ -378,7 +392,8 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
 	}
 
 	// serverContent will check modification time
-	serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
+	sizeFunc := func() (int64, error) { return d.Size(), nil }
+	serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
 }
 
 // localRedirect gives a Moved Permanently response.

src/pkg/net/http/fs_test.go

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -567,7 +567,10 @@ func TestServeContent(t *testing.T) {
 	defer ts.Close()\n
 	type testCase struct {
-\t\tfile             string
+\t\t// One of file or content must be set:
+\t\tfile    string
+\t\tcontent io.ReadSeeker
+\n
 \t\tmodtime          time.Time
 \t\tserveETag        string // optional
 \t\tserveContentType string // optional
@@ -615,6 +618,14 @@ func TestServeContent(t *testing.T) {
 \t\t\t},\n
 \t\t\twantStatus: 304,\n
 \t\t},\n
+\t\t\"not_modified_etag_no_seek\": {\n
+\t\t\tcontent:   panicOnSeek{nil}, // should never be called\n
+\t\t\tserveETag: `\"foo\"`,\n
+\t\t\treqHeader: map[string]string{\n
+\t\t\t\t\"If-None-Match\": `\"foo\"`,\n
+\t\t\t},\n
+\t\t\twantStatus: 304,\n
+\t\t},\n
 \t\t\"range_good\": {\n
 \t\t\tfile:      \"testdata/style.css\",\n
 \t\t\tserveETag: `\"A\"`,\n
@@ -638,15 +649,21 @@ func TestServeContent(t *testing.T) {
 \t\t},\n
 \t}\n
 \tfor testName, tt := range tests {\n
-\t\tf, err := os.Open(tt.file)\n
-\t\tif err != nil {\n
-\t\t\tt.Fatalf(\"test %q: %v\", testName, err)\n
+\t\tvar content io.ReadSeeker\n
+\t\tif tt.file != \"\" {\n
+\t\t\tf, err := os.Open(tt.file)\n
+\t\t\tif err != nil {\n
+\t\t\t\tt.Fatalf(\"test %q: %v\", testName, err)\n
+\t\t\t}\n
+\t\t\tdefer f.Close()\n
+\t\t\tcontent = f\n+\t\t} else {\n
+\t\t\tcontent = tt.content\n \t\t}\n-\t\tdefer f.Close()\n
 \n \t\tservec <- serveParam{\n \t\t\tname:        filepath.Base(tt.file),\n-\t\t\tcontent:     f,\n+\t\t\tcontent:     content,\n \t\t\tmodtime:     tt.modtime,\n \t\t\tetag:        tt.serveETag,\n \t\t\tcontentType: tt.serveContentType,\n@@ -763,3 +780,5 @@ func TestLinuxSendfileChild(*testing.T) {\n \t\tpanic(err)\n \t}\n }\n+\n+type panicOnSeek struct{ io.ReadSeeker }\n```

## コアとなるコードの解説

### `ServeContent` 関数の変更

*   **`size` 引数の削除と `sizeFunc` の導入**:
    変更前は `ServeContent` が直接 `content.Seek` を呼び出して `size` を計算し、その `size` を `serveContent` に渡していました。
    変更後は、`sizeFunc := func() (int64, error) { ... }` という匿名関数を定義し、この関数内で `Seek` 操作を実行するようにしました。この `sizeFunc` が `serveContent` に渡されます。これにより、`Seek` 操作の実行タイミングを制御できるようになります。
*   **エラーハンドリングの改善**:
    `sizeFunc` 内で `content.Seek` がエラーを返した場合、直接そのエラーを返すのではなく、新しく定義された内部エラー `errSeeker` を返すようにしました。これは、`Seek` 操作の具体的なエラーメッセージがHTTPレスポンスとしてクライアントに漏洩するのを防ぐためです。

### `serveContent` 関数の変更

*   **`sizeFunc` の呼び出しタイミングの変更**:
    変更前は `serveContent` が `size` を直接引数として受け取っていました。
    変更後は `sizeFunc` を引数として受け取り、`checkLastModified(w, r, modtime)` の呼び出し後、かつ `304 Not Modified` レスポンスが返されない場合にのみ `size, err := sizeFunc()` を呼び出すようにしました。これにより、キャッシュがヒットして `304` が返される場合には、`Seek` 操作が完全にスキップされます。

### `serveFile` 関数の変更

*   `serveFile` 関数も `ServeContent` を呼び出しているため、その引数に合わせて `sizeFunc` を生成して渡すように変更されました。`os.File` の `Size()` メソッドは `Seek` を必要としないため、シンプルな `sizeFunc` が定義されています。

### テストコード (`fs_test.go`) の変更

*   **`testCase` 構造体の拡張**:
    テストケースの定義に `content io.ReadSeeker` フィールドが追加され、ファイルパスだけでなく、直接 `io.ReadSeeker` の実装をテストに渡せるようになりました。
*   **`panicOnSeek` 構造体の追加**:
    `io.ReadSeeker` インターフェースを実装し、`Seek` メソッドが呼び出されると `panic` を発生させる `panicOnSeek` 型が追加されました。
*   **`not_modified_etag_no_seek` テストケースの追加**:
    この新しいテストケースでは、`panicOnSeek` を `content` として使用し、`If-None-Match` ヘッダーを送信して `304 Not Modified` レスポンスが期待される状況をシミュレートします。このテストが成功するということは、`304` レスポンスの場合に `Seek` メソッドが実際に呼び出されていないことを証明します。もし `Seek` が呼び出されれば、`panicOnSeek` によってテストは失敗します。

これらの変更により、`net/http.ServeContent` はより効率的で堅牢になり、特にキャッシュが有効に機能するシナリオにおいて、不要なI/Oオーバーヘッドを削減できるようになりました。

## 関連リンク

*   Go言語 `io` パッケージのドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
*   Go言語 `net/http` パッケージのドキュメント: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
*   HTTP/1.1 RFC 2616 (特にセクション 14.25 If-Modified-Since, 14.26 If-None-Match, 10.3.5 304 Not Modified): [https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25)

## 参考にした情報源リンク

*   Go CL 11080043: [https://golang.org/cl/11080043](https://golang.org/cl/11080043) (元の変更リスト)
*   Go issue tracker (関連する可能性のあるissue): Goのコミットメッセージには直接issue番号が記載されていませんが、通常は関連するissueが存在します。この変更はパフォーマンスと堅牢性の改善であるため、そのような目的のissueが背景にある可能性があります。