[インデックス 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.ResponseWriter
が io.ReaderFrom
インターフェースを実装している場合(例えば、io.Copy
を使用してレスポンスボディを書き込む場合)、このスニッフィングのメカニズムが正しく機能しないという問題がありました。
元の実装では、response.ReadFrom
メソッド内で w.Flush()
が最初に呼び出されていました。Flush()
は、まだヘッダが書き込まれていない場合に WriteHeader
を呼び出し、その際に w.chunking
フラグを設定します。しかし、この Flush()
の呼び出しが、コンテンツタイプスニッフィングに必要なボディの最初のバイトが読み込まれる前にヘッダを書き込んでしまう可能性がありました。
具体的には、w.needSniff
が true
の場合(つまり、コンテンツタイプスニッフィングが必要な場合)、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()
呼び出しにありました。
-
元のロジック:
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.wroteHeader
がfalse
であれば、Flush()
は内部的にw.WriteHeader(StatusOK)
を呼び出します。WriteHeader
が呼び出されると、レスポンスヘッダが確定し、Content-Type
ヘッダがまだ設定されていない場合は、デフォルトのtext/plain; charset=utf-8
が設定されてしまいます。 しかし、ReadFrom
の目的はsrc
からデータを読み込んでレスポンスボディとして書き込むことです。もしw.needSniff
がtrue
であれば、本来はsrc
から最初の数バイトを読み取ってコンテンツタイプをスニッフィングし、その結果に基づいてContent-Type
ヘッダを設定する必要があります。Flush()
が先にヘッダを確定させてしまうと、このスニッフィングの機会が失われていました。 -
修正後のロジック:
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.needSniff
がtrue
の場合、つまりコンテンツタイプスニッフィングが必要な場合は、このif
ブロックに入らないことです。 w.needSniff
がtrue
の場合、ReadFrom
はio.ReaderFrom
を利用した最適化されたコピーパスに入らず、通常のio.Copy
のようなパス(内部的にはio.Writer
インターフェースを通じてバイトを書き込む)に進みます。このパスでは、Write
メソッドが呼び出される際に、ボディの最初のバイトに基づいてコンテンツタイプスニッフィングが実行され、適切なContent-Type
ヘッダが設定されます。w.Flush()
は、io.ReaderFrom
を利用した最適化されたコピーパスに入る直前に移動されました。このパスに入るということは、w.needSniff
がfalse
である(つまり、スニッフィングが不要であるか、既に行われている)ことを意味するため、この時点での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.needSniff
がtrue
の場合)でも、ヘッダを確定させますが、スニッフィング自体はWrite
メソッドが呼び出される際に後で行われます。- 元の
w.Flush()
の呼び出しは、if !w.chunking && w.bodyAllowed() && !w.needSniff
の条件ブロック内に移動されました。この条件は、io.ReaderFrom
を利用した最適化されたコピーパスに入る場合にのみ真となります。このパスに入るということは、w.needSniff
がfalse
である(つまり、スニッフィングが不要であるか、既に行われている)ことを意味するため、この時点でのFlush()
は問題なく、チャンク転送の開始などを適切に処理できます。
src/pkg/net/http/sniff_test.go
の変更
TestContentTypeWithCopy
という新しいテスト関数が追加されました。- このテストは、
httptest.NewServer
を使用してHTTPサーバーをセットアップします。 - サーバーのハンドラ内で、
io.Copy(w, buf)
を使用してレスポンスボディを書き込んでいます。ここでw
はhttp.ResponseWriter
であり、io.ReaderFrom
インターフェースを実装しているため、ReadFrom
メソッドが内部的にトリガーされます。 input
としてHTMLの断片(\n<html>\n\t<head>\n
)が用意されており、期待されるContent-Type
はtext/html; charset=utf-8
です。- テストは、
Get
リクエストを送信し、返されたレスポンスのContent-Type
ヘッダが期待される値と一致するかどうかを確認します。また、レスポンスボディの内容も検証します。 - このテストの追加により、
ReadFrom
を使用した場合のコンテンツタイプスニッフィングの修正が正しく機能していることが保証されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/9c6a73e478e6e46859c68057144b8c3297e7a881
golang.org/cl/5362046
: Web検索ではこのCLに関する公開情報を見つけることができませんでした。
参考にした情報源リンク
- コミット情報 (
@commit_data/10302.txt
) - Go言語の
net/http
パッケージのドキュメント (一般的な知識として) io.ReaderFrom
インターフェースのドキュメント (一般的な知識として)- HTTP Content-Type スニッフィングに関する一般的な情報 (一般的な知識として)
io.Copy
関数の動作に関する一般的な情報 (一般的な知識として)