[インデックス 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関数の動作に関する一般的な情報 (一般的な知識として)