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

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

このコミットは、Go言語の net/http パッケージにおける、ReadFrom メソッド使用時のコンテンツタイプスニッフィングの不具合を修正するものです。具体的には、HTTPレスポンスのボディを io.ReaderFrom インターフェースを通じて書き込む際に、コンテンツタイプが正しく検出されない問題を解決します。この修正により、io.Copy のような操作でレスポンスボディを送信した場合でも、適切な Content-Type ヘッダが設定されるようになります。

コミット

  • コミットハッシュ: 9c6a73e478e6e46859c68057144b8c3297e7a881
  • Author: David Symonds dsymonds@golang.org
  • Date: Wed Nov 9 15:48:05 2011 +1100

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

https://github.com/golang/go/commit/9c6a73e478e6e46859c68057144b8c3297e7a881

元コミット内容

    net/http: fix sniffing when using ReadFrom.
    
    R=golang-dev, rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/5362046

変更の背景

Goの net/http パッケージでは、HTTPレスポンスの Content-Type ヘッダが明示的に設定されていない場合、レスポンスボディの最初の数バイトを「スニッフィング(嗅ぎ分け)」して、適切なコンテンツタイプを推測する機能があります。これは、ブラウザなどがコンテンツを正しく解釈するために重要です。

しかし、http.ResponseWriterio.ReaderFrom インターフェースを実装している場合(例えば、io.Copy を使用してレスポンスボディを書き込む場合)、このスニッフィングのメカニズムが正しく機能しないという問題がありました。

元の実装では、response.ReadFrom メソッド内で w.Flush() が最初に呼び出されていました。Flush() は、まだヘッダが書き込まれていない場合に WriteHeader を呼び出し、その際に w.chunking フラグを設定します。しかし、この Flush() の呼び出しが、コンテンツタイプスニッフィングに必要なボディの最初のバイトが読み込まれる前にヘッダを書き込んでしまう可能性がありました。

具体的には、w.needSnifftrue の場合(つまり、コンテンツタイプスニッフィングが必要な場合)、Flush()WriteHeader を呼び出すことで、スニッフィングが行われる前にヘッダが確定してしまい、結果として Content-Type ヘッダが text/plain; charset=utf-8 のようなデフォルト値になってしまうことがありました。このコミットは、この問題を解決し、ReadFrom を使用した場合でもコンテンツタイプスニッフィングが正しく行われるようにすることを目的としています。

前提知識の解説

  • net/http パッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。
  • http.ResponseWriter: HTTPレスポンスを構築するためのインターフェースです。ヘッダの設定やボディの書き込みを行います。
  • io.ReaderFrom インターフェース: ReadFrom(r Reader) (n int64, err error) メソッドを持つインターフェースです。このインターフェースを実装する型は、別の io.Reader からデータを効率的に読み込むことができます。io.Copy 関数は、書き込み先が io.ReaderFrom を実装している場合、このメソッドを利用して最適化されたコピーを行います。
  • コンテンツタイプスニッフィング (Content Type Sniffing): HTTPレスポンスの Content-Type ヘッダが明示的に指定されていない場合に、レスポンスボディの最初の数バイトを調べて、その内容から適切なMIMEタイプ(例: text/html, image/png)を推測するメカニズムです。これにより、ブラウザは受信したデータを正しくレンダリングできます。
  • w.WriteHeader(statusCode int): http.ResponseWriter のメソッドで、HTTPレスポンスのステータスコードとヘッダをクライアントに送信します。このメソッドが呼び出されると、ヘッダの変更はできなくなります。
  • w.Flush(): http.ResponseWriter のメソッドで、バッファリングされているレスポンスデータを強制的にクライアントに送信します。ヘッダがまだ書き込まれていない場合は、WriteHeader(http.StatusOK) が暗黙的に呼び出されます。
  • w.chunking: HTTPレスポンスがチャンク転送エンコーディングを使用しているかどうかを示す内部フラグです。通常、ボディのサイズが不明な場合や、ストリーミングでデータを送信する場合に設定されます。
  • w.bodyAllowed(): HTTPメソッドやステータスコードに基づいて、レスポンスボディが許可されているかどうかを判断する内部ヘルパー関数です。
  • w.needSniff: コンテンツタイプスニッフィングが必要かどうかを示す内部フラグです。Content-Type ヘッダがまだ設定されていない場合に true になります。

技術的詳細

この問題の核心は、response.ReadFrom メソッドの初期の w.Flush() 呼び出しにありました。

  1. 元のロジック:

    func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
        // Flush before checking w.chunking, as Flush will call
        // WriteHeader if it hasn't been called yet, and WriteHeader
        // is what sets w.chunking.
        w.Flush()
        if !w.chunking && w.bodyAllowed() && !w.needSniff {
            // ... (io.ReaderFrom を利用したコピー)
        }
        // ...
    }
    

    このコードでは、ReadFrom が呼び出されるとすぐに w.Flush() が実行されます。もし w.wroteHeaderfalse であれば、Flush() は内部的に w.WriteHeader(StatusOK) を呼び出します。WriteHeader が呼び出されると、レスポンスヘッダが確定し、Content-Type ヘッダがまだ設定されていない場合は、デフォルトの text/plain; charset=utf-8 が設定されてしまいます。 しかし、ReadFrom の目的は src からデータを読み込んでレスポンスボディとして書き込むことです。もし w.needSnifftrue であれば、本来は src から最初の数バイトを読み取ってコンテンツタイプをスニッフィングし、その結果に基づいて Content-Type ヘッダを設定する必要があります。Flush() が先にヘッダを確定させてしまうと、このスニッフィングの機会が失われていました。

  2. 修正後のロジック:

    func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
        // Call WriteHeader before checking w.chunking if it hasn't
        // been called yet, since WriteHeader is what sets w.chunking.
        if !w.wroteHeader {
            w.WriteHeader(StatusOK)
        }
        if !w.chunking && w.bodyAllowed() && !w.needSniff {
            w.Flush() // Flush moved here
            if rf, ok := w.conn.rwc.(io.ReaderFrom); ok {
                n, err = rf.ReadFrom(src)
                w.written += n
            }
        }
        // ...
    }
    

    修正では、w.Flush() の呼び出しが if !w.chunking && w.bodyAllowed() && !w.needSniff の条件ブロック内に移動されました。

    • まず、if !w.wroteHeader { w.WriteHeader(StatusOK) } が追加されました。これは、ヘッダがまだ書き込まれていない場合にのみ WriteHeader(StatusOK) を明示的に呼び出すものです。これにより、w.chunking の状態を正しく設定できます。
    • 重要なのは、w.needSnifftrue の場合、つまりコンテンツタイプスニッフィングが必要な場合は、この if ブロックに入らないことです。
    • w.needSnifftrue の場合、ReadFromio.ReaderFrom を利用した最適化されたコピーパスに入らず、通常の io.Copy のようなパス(内部的には io.Writer インターフェースを通じてバイトを書き込む)に進みます。このパスでは、Write メソッドが呼び出される際に、ボディの最初のバイトに基づいてコンテンツタイプスニッフィングが実行され、適切な Content-Type ヘッダが設定されます。
    • w.Flush() は、io.ReaderFrom を利用した最適化されたコピーパスに入る直前に移動されました。このパスに入るということは、w.needSnifffalse である(つまり、スニッフィングが不要であるか、既に行われている)ことを意味するため、この時点での Flush() は問題ありません。

この変更により、ReadFrom を使用してレスポンスボディを書き込む場合でも、Content-Type ヘッダが明示的に設定されていない限り、コンテンツタイプスニッフィングが正しく機能するようになりました。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -149,11 +149,13 @@ type writerOnly struct {
 }
 
 func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
-	// Flush before checking w.chunking, as Flush will call
-	// WriteHeader if it hasn't been called yet, and WriteHeader
-	// is what sets w.chunking.
-	w.Flush()
+	// Call WriteHeader before checking w.chunking if it hasn't
+	// been called yet, since WriteHeader is what sets w.chunking.
+	if !w.wroteHeader {
+		w.WriteHeader(StatusOK)
+	}
 	if !w.chunking && w.bodyAllowed() && !w.needSniff {
+		w.Flush()
 		if rf, ok := w.conn.rwc.(io.ReaderFrom); ok {
 			n, err = rf.ReadFrom(src)
 			w.written += n

src/pkg/net/http/sniff_test.go

--- a/src/pkg/net/http/sniff_test.go
+++ b/src/pkg/net/http/sniff_test.go
@@ -6,6 +6,7 @@ package http_test
 
 import (
 	"bytes"
+	"io"
 	"io/ioutil"
 	"log"
 	. "net/http"
@@ -79,3 +80,35 @@ func TestServerContentType(t *testing.T) {
 		resp.Body.Close()
 	}
 }
+
+func TestContentTypeWithCopy(t *testing.T) {
+	const (
+		input    = "\n<html>\n\t<head>\n"
+		expected = "text/html; charset=utf-8"
+	)
+
+	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+		// Use io.Copy from a bytes.Buffer to trigger ReadFrom.
+		buf := bytes.NewBuffer([]byte(input))
+		n, err := io.Copy(w, buf)
+		if int(n) != len(input) || err != nil {
+			t.Fatalf("io.Copy(w, %q) = %v, %v want %d, nil", input, n, err, len(input))
+		}
+	}))
+	defer ts.Close()
+
+	resp, err := Get(ts.URL)
+	if err != nil {
+		t.Fatalf("Get: %v", err)
+	}
+	if ct := resp.Header.Get("Content-Type"); ct != expected {
+		t.Errorf("Content-Type = %q, want %q", ct, expected)
+	}
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Errorf("reading body: %v", err)
+	} else if !bytes.Equal(data, []byte(input)) {
+		t.Errorf("data is %q, want %q", data, input)
+	}
+	resp.Body.Close()
+}

コアとなるコードの解説

src/pkg/net/http/server.go の変更

  • 変更前: w.Flush()ReadFrom メソッドの冒頭で無条件に呼び出されていました。
  • 変更後:
    • if !w.wroteHeader { w.WriteHeader(StatusOK) } が追加されました。これは、ヘッダがまだ書き込まれていない場合にのみ WriteHeader(StatusOK) を明示的に呼び出すことで、w.chunking フラグが正しく設定されるようにします。この呼び出しは、コンテンツタイプスニッフィングが必要な場合(w.needSnifftrue の場合)でも、ヘッダを確定させますが、スニッフィング自体は Write メソッドが呼び出される際に後で行われます。
    • 元の w.Flush() の呼び出しは、if !w.chunking && w.bodyAllowed() && !w.needSniff の条件ブロック内に移動されました。この条件は、io.ReaderFrom を利用した最適化されたコピーパスに入る場合にのみ真となります。このパスに入るということは、w.needSnifffalse である(つまり、スニッフィングが不要であるか、既に行われている)ことを意味するため、この時点での Flush() は問題なく、チャンク転送の開始などを適切に処理できます。

src/pkg/net/http/sniff_test.go の変更

  • TestContentTypeWithCopy という新しいテスト関数が追加されました。
  • このテストは、httptest.NewServer を使用してHTTPサーバーをセットアップします。
  • サーバーのハンドラ内で、io.Copy(w, buf) を使用してレスポンスボディを書き込んでいます。ここで whttp.ResponseWriter であり、io.ReaderFrom インターフェースを実装しているため、ReadFrom メソッドが内部的にトリガーされます。
  • input としてHTMLの断片(\n<html>\n\t<head>\n)が用意されており、期待される Content-Typetext/html; charset=utf-8 です。
  • テストは、Get リクエストを送信し、返されたレスポンスの Content-Type ヘッダが期待される値と一致するかどうかを確認します。また、レスポンスボディの内容も検証します。
  • このテストの追加により、ReadFrom を使用した場合のコンテンツタイプスニッフィングの修正が正しく機能していることが保証されます。

関連リンク

参考にした情報源リンク

  • コミット情報 (@commit_data/10302.txt)
  • Go言語の net/http パッケージのドキュメント (一般的な知識として)
  • io.ReaderFrom インターフェースのドキュメント (一般的な知識として)
  • HTTP Content-Type スニッフィングに関する一般的な情報 (一般的な知識として)
  • io.Copy 関数の動作に関する一般的な情報 (一般的な知識として)