[インデックス 17103] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージにおけるResponseWriter
のReadFrom
メソッドの挙動を修正するものです。具体的には、ReadFrom
がデータが読み込まれる前に副作用(HTTPヘッダの書き込みなど)を引き起こす問題を解決し、sendfile
のような最適化が適用できない場合に通常のio.Copy
にフォールバックするロジックを改善しています。
コミット
- コミットハッシュ:
de04bf24e5a9d4a52f0024dd684de21ec4a36340
- Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Thu Aug 8 14:02:54 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/de04bf24e5a9d4a52f0024dd684de21ec4a36340
元コミット内容
net/http: fix early side effects in the ResponseWriter's ReadFrom
The ResponseWriter's ReadFrom method was causing side effects on
the output before any data was read.
Now, bail out early and do a normal copy (which does a read
before writing) when our input and output are known to not to
be the pair of types we need for sendfile.
Fixes #5660
R=golang-dev, rsc, nightlyone
CC=golang-dev
https://golang.org/cl/12632043
変更の背景
このコミットは、Goのnet/http
パッケージにおけるResponseWriter.ReadFrom
メソッドが抱えていた問題、具体的にはgolang.org/issue/5660で報告されたバグを修正するために導入されました。
問題の核心は、ResponseWriter.ReadFrom
が、実際にデータがソースから読み込まれる前に、HTTPレスポンスヘッダを早期に書き込んでしまう可能性があった点にあります。これは、特にio.Pipe
のようなブロッキングI/Oを使用している場合や、レスポンスが別のゴルーチンで生成されている場合に問題となりました。例えば、リクエストボディを読み込んでいる最中に、レスポンスヘッダが先に書き込まれてしまうと、予期せぬ動作や競合状態が発生する可能性がありました。
HTTPプロトコルでは、レスポンスヘッダはボディの前に送信される必要がありますが、ReadFrom
が最適化のために内部的にsendfile
のようなゼロコピー操作を利用しようとする際、ソースからのデータがまだ利用可能でないにもかかわらず、ヘッダをフラッシュしてしまうことがありました。これにより、コンテンツタイプ推測(Content-Type sniffing)などの処理が正しく行われなくなる可能性もありました。
このコミットは、このような早期の副作用を防ぎ、sendfile
のような特定の最適化が適用できない状況では、より安全で予測可能な通常のio.Copy
の動作にフォールバックするようにReadFrom
のロジックを変更することを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語のI/Oおよびネットワークに関する概念の理解が不可欠です。
-
io.ReaderFrom
インターフェース:io.ReaderFrom
インターフェースは、ReadFrom(r Reader) (n int64, err error)
メソッドを定義します。このインターフェースを実装する型は、別のio.Reader
からデータを効率的に読み込む能力があることを示します。特に、io.Copy
関数は、コピー元がio.ReaderFrom
を実装している場合、そのReadFrom
メソッドを呼び出して最適化されたコピー処理を試みます。これにより、中間バッファを介さずに直接データを転送できる場合があります(例:sendfile
システムコール)。 -
io.Copy
関数:io.Copy(dst Writer, src Reader) (written int64, err error)
は、src
からdst
へデータをコピーするGoの標準関数です。この関数は、コピー元がio.ReaderFrom
を実装している場合、またはコピー先がio.WriterTo
を実装している場合に、それらの最適化されたメソッドを利用しようとします。それ以外の場合は、内部的にバッファを使用してデータを読み書きします。 -
net/http
パッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための標準パッケージです。http.ResponseWriter
: HTTPレスポンスを構築するためにサーバーハンドラが使用するインターフェースです。ヘッダの設定、ステータスコードの書き込み、レスポンスボディの書き込みなどを行います。io.Writer
インターフェースも実装しています。http.Request
: 受信したHTTPリクエストを表す構造体です。リクエストメソッド、URL、ヘッダ、ボディなどの情報を含みます。
-
sendfile
システムコール(またはsplice
):sendfile
は、Unix系OSで利用可能なシステムコールで、カーネル空間内でファイルディスクリプタからソケットディスクリプタへデータを直接転送する機能を提供します。これにより、ユーザー空間へのデータコピーが不要になり、I/Oのオーバーヘッドが大幅に削減されます。これは「ゼロコピー」I/Oの一種です。net/http
パッケージのResponseWriter.ReadFrom
は、特定の条件下でこのsendfile
のような最適化を利用しようとします。 -
コンテンツタイプ推測(Content-Type Sniffing): HTTPレスポンスにおいて、
Content-Type
ヘッダが明示的に設定されていない場合、ブラウザやクライアントはレスポンスボディの最初の数バイトを検査(スニッフィング)して、そのコンテンツタイプ(例:text/html
,image/png
など)を推測しようとします。この推測は、セキュリティ上の理由(例: XSS攻撃の防止)や、正しいレンダリングのために重要です。ResponseWriter.ReadFrom
が早期にヘッダを書き込んでしまうと、このスニッフィングが正しく行われなくなる可能性がありました。
技術的詳細
このコミットは、net/http
パッケージのresponse
構造体(http.ResponseWriter
の実装)のReadFrom
メソッドの動作を根本的に変更しています。
以前のReadFrom
の実装では、response
がio.ReaderFrom
を実装している場合(これは通常、基盤となるネットワーク接続がio.ReaderFrom
を実装している場合に該当します)、まずw.WriteHeader(StatusOK)
を呼び出してHTTPステータスコードとヘッダを書き込み、その後、基盤となる接続のReadFrom
メソッドを直接呼び出してsendfile
のような最適化を試みていました。
この挙動の問題点は、w.WriteHeader(StatusOK)
が呼び出されると、たとえソースsrc
からまだデータが読み込まれていなくても、レスポンスヘッダがクライアントに送信されてしまうことでした。これにより、以下のような問題が発生する可能性がありました。
- 早期のヘッダ送信:
src
がブロッキングI/O(例:io.Pipe
)である場合、データが利用可能になる前にヘッダが送信されてしまい、クライアントが不完全なレスポンスを受け取るか、コンテンツタイプ推測が正しく行われない。 - 競合状態: サーバーハンドラがリクエストボディを読み込んでいる最中に、別のゴルーチンが
ReadFrom
を呼び出してレスポンスヘッダを送信してしまうと、予期せぬ動作を引き起こす。
このコミットによる修正は、この早期の副作用を防ぐために、sendfile
のような最適化が利用できる条件をより厳密にチェックするように変更しました。
新しいロジックでは、response.ReadFrom
は以下の条件をチェックします。
- 出力先が
io.ReaderFrom
を実装しているか?:w.conn.rwc
(通常は*net.TCPConn
)がio.ReaderFrom
インターフェースを実装しているかを確認します。これはsendfile
のような最適化が可能であるための前提条件です。 - 入力元が通常のファイルであるか?: 新しく導入されたヘルパー関数
srcIsRegularFile(src io.Reader)
を使用して、入力元src
が通常のファイル(*os.File
)であるかどうかをチェックします。これは、sendfile
が主にファイルディスクリプタからソケットディスクリプタへの転送に最適化されているためです。
これらの両方の条件が満たされない場合(つまり、出力先がio.ReaderFrom
を実装していない、または入力元が通常のファイルではない場合)、ReadFrom
は早期に処理を中断し、io.Copy(writerOnly{w}, src)
を呼び出して通常のコピー処理にフォールバックします。
writerOnly
構造体は、io.Writer
インターフェースのみを公開するラッパーです。これは、io.Copy
がコピー元またはコピー先がio.ReaderFrom
またはio.WriterTo
を実装している場合に最適化されたパスを試みるのを防ぐために使用されます。writerOnly{w}
をio.Copy
に渡すことで、io.Copy
はw
がReadFrom
メソッドを持っていることを認識せず、常にバッファリングされた通常のコピー処理を実行します。これにより、ReadFrom
の内部でio.Copy
が再帰的にReadFrom
を呼び出すことを防ぎ、予測可能な動作を保証します。
sendfileパス:
上記の条件が満たされ、sendfile
のような最適化が可能な場合のみ、w.WriteHeader(StatusOK)
を呼び出してヘッダを書き込み、その後、基盤となる接続のrf.ReadFrom(src)
を呼び出して最適化されたコピー処理を実行します。これにより、ヘッダの書き込みは、sendfile
が実際に実行される直前に行われるようになります。
この変更により、ReadFrom
は、sendfile
が利用できない状況では安全に通常のコピーにフォールバックし、早期の副作用を防ぐことができます。
コアとなるコードの変更箇所
このコミットによる主要な変更は、以下の2つのファイルに集中しています。
src/pkg/net/http/serve_test.go
: 新しいテストケースTestServerReaderFromOrder
が追加されました。src/pkg/net/http/server.go
:response
構造体のReadFrom
メソッドのロジックが変更され、writerOnly
構造体とsrcIsRegularFile
ヘルパー関数が追加されました。
src/pkg/net/http/serve_test.go
の変更
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1815,6 +1815,51 @@ func TestHTTP10ConnectionHeader(t *testing.T) {
}
}
+// See golang.org/issue/5660
+func TestServerReaderFromOrder(t *testing.T) {
+ defer afterTest(t)
+ pr, pw := io.Pipe()
+ const size = 3 << 20
+ ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {
+ rw.Header().Set("Content-Type", "text/plain") // prevent sniffing path
+ done := make(chan bool)
+ go func() {
+ io.Copy(rw, pr)
+ close(done)
+ }()
+ time.Sleep(25 * time.Millisecond) // give Copy a chance to break things
+ n, err := io.Copy(ioutil.Discard, req.Body)
+ if err != nil {
+ t.Errorf("handler Copy: %v", err)
+ return
+ }
+ if n != size {
+ t.Errorf("handler Copy = %d; want %d", n, size)
+ }
+ pw.Write([]byte("hi"))
+ pw.Close()
+ <-done
+ }))
+ defer ts.Close()
+
+ req, err := NewRequest("POST", ts.URL, io.LimitReader(neverEnding('a'), size))
+ if err != nil {
+ t.Fatal(err)
+ }
+ res, err := DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ all, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ if string(all) != "hi" {
+ t.Errorf("Body = %q; want hi", all)
+ }
+}
+
func BenchmarkClientServer(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
src/pkg/net/http/server.go
の変更
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -16,6 +16,7 @@ import (
"log"
"net"
"net/url"
+ "os"
"path"
"runtime"
"strconv"
@@ -345,11 +346,44 @@ func (w *response) needsSniff() bool {
return !w.cw.wroteHeader && w.handlerHeader.Get("Content-Type") == "" && w.written < sniffLen
}
+// writerOnly hides an io.Writer value's optional ReadFrom method
+// from io.Copy.
type writerOnly struct {
io.Writer
}
+func srcIsRegularFile(src io.Reader) (isRegular bool, err error) {
+ switch v := src.(type) {
+ case *os.File:
+ fi, err := v.Stat()
+ if err != nil {
+ return false, err
+ }
+ return fi.Mode().IsRegular(), nil
+ case *io.LimitedReader:
+ return srcIsRegularFile(v.R)
+ default:
+ return
+ }
+}
+
+// ReadFrom is here to optimize copying from an *os.File regular file
+// to a *net.TCPConn with sendfile.
func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
+ // Our underlying w.conn.rwc is usually a *TCPConn (with its
+ // own ReadFrom method). If not, or if our src isn't a regular
+ // file, just fall back to the normal copy method.
+ rf, ok := w.conn.rwc.(io.ReaderFrom)
+ regFile, err := srcIsRegularFile(src)
+ if err != nil {
+ return 0, err
+ }
+ if !ok || !regFile {
+ return io.Copy(writerOnly{w}, src)
+ }
+
+ // sendfile path:
+
if !w.wroteHeader {
w.WriteHeader(StatusOK)
}
@@ -367,16 +401,12 @@ func (w *response) ReadFrom(src io.Reader) (n int64, err error) {
// Now that cw has been flushed, its chunking field is guaranteed initialized.
if !w.cw.chunking && w.bodyAllowed() {
- if rf, ok := w.conn.rwc.(io.ReaderFrom); ok {
- n0, err := rf.ReadFrom(src)
- n += n0
- w.written += n0
- return n, err
- }
+ n0, err := rf.ReadFrom(src)
+ n += n0
+ w.written += n0
+ return n, err
}
- // Fall back to default io.Copy implementation.
- // Use wrapper to hide w.ReadFrom from io.Copy.
n0, err := io.Copy(writerOnly{w}, src)
n += n0
return n, err
コアとなるコードの解説
src/pkg/net/http/server.go
-
writerOnly
構造体:type writerOnly struct { io.Writer }
この構造体は、
io.Writer
インターフェースを埋め込むことで、io.Writer
のメソッド(Write
)のみを公開します。response
構造体自体はio.Writer
とio.ReaderFrom
の両方を実装していますが、io.Copy(writerOnly{w}, src)
のようにwriterOnly
でラップして渡すことで、io.Copy
がw
のReadFrom
メソッドを認識せず、通常のバッファリングされたコピー処理を実行するように強制します。これにより、ReadFrom
の内部でio.Copy
が再帰的にReadFrom
を呼び出すことを防ぎ、無限ループや予期せぬ動作を回避します。 -
srcIsRegularFile
関数:func srcIsRegularFile(src io.Reader) (isRegular bool, err error) { switch v := src.(type) { case *os.File: fi, err := v.Stat() if err != nil { return false, err } return fi.Mode().IsRegular(), nil case *io.LimitedReader: return srcIsRegularFile(v.R) default: return } }
このヘルパー関数は、与えられた
io.Reader
が通常のファイル(*os.File
)であるかどうかを再帰的にチェックします。*os.File
の場合、Stat()
を呼び出してファイル情報を取得し、fi.Mode().IsRegular()
で通常のファイルであるかを確認します。*io.LimitedReader
の場合、その内部のio.Reader
に対して再帰的にsrcIsRegularFile
を呼び出します。これは、io.LimitReader
でラップされたファイルも正しく扱えるようにするためです。- その他の型の場合、通常のファイルではないと判断し、
false
を返します。 この関数は、sendfile
のような最適化がファイルディスクリプタに特化しているため、入力元が通常のファイルであるかどうかの条件を厳密にチェックするために使用されます。
-
response.ReadFrom
メソッドの変更:func (w *response) ReadFrom(src io.Reader) (n int64, err error) { // Our underlying w.conn.rwc is usually a *TCPConn (with its // own ReadFrom method). If not, or if our src isn't a regular // file, just fall back to the normal copy method. rf, ok := w.conn.rwc.(io.ReaderFrom) regFile, err := srcIsRegularFile(src) if err != nil { return 0, err } if !ok || !regFile { return io.Copy(writerOnly{w}, src) } // sendfile path: if !w.wroteHeader { w.WriteHeader(StatusOK) } // ... (既存のsendfileパスのロジック) ... }
これが最も重要な変更点です。
- まず、
w.conn.rwc
(基盤となるネットワーク接続、通常は*net.TCPConn
)がio.ReaderFrom
を実装しているか(rf, ok := w.conn.rwc.(io.ReaderFrom)
)と、入力元src
がsrcIsRegularFile
によって通常のファイルであるか(regFile, err := srcIsRegularFile(src)
)をチェックします。 !ok || !regFile
: これらの条件のいずれかが満たされない場合(つまり、出力先がio.ReaderFrom
を実装していないか、入力元が通常のファイルではない場合)、io.Copy(writerOnly{w}, src)
を呼び出して、sendfile
のような最適化を試みずに通常のコピー処理にフォールバックします。これにより、早期のヘッダ書き込みを防ぎ、安全な動作を保証します。sendfile path
: 上記の条件が満たされた場合のみ、sendfile
による最適化が可能なパスに進みます。このパスでは、!w.wroteHeader
のチェック後にw.WriteHeader(StatusOK)
を呼び出し、その後、基盤となる接続のrf.ReadFrom(src)
を呼び出します。これにより、ヘッダの書き込みは、実際にデータ転送が開始される直前に行われるようになります。
- まず、
src/pkg/net/http/serve_test.go
TestServerReaderFromOrder
テストケース: この新しいテストケースは、golang.org/issue/5660で報告された問題を再現し、修正が正しく機能することを確認するために追加されました。io.Pipe
を使用して、書き込み側(pw
)と読み込み側(pr
)を作成します。- HTTPハンドラ内で、別のゴルーチンで
io.Copy(rw, pr)
(ResponseWriter
にパイプからコピー)を実行します。 - メインのハンドラゴルーチンでは、
time.Sleep(25 * time.Millisecond)
で意図的に遅延を入れ、io.Copy(ioutil.Discard, req.Body)
でリクエストボディを読み込みます。 - その後、
pw.Write([]byte("hi"))
でパイプにデータを書き込み、pw.Close()
で閉じます。 - このテストの目的は、リクエストボディの読み込みが完了する前に、
ResponseWriter.ReadFrom
がレスポンスヘッダを書き込んでしまうという以前のバグを検出することです。修正後、ReadFrom
はパイプからのデータが利用可能になるまでヘッダの書き込みを遅延させるため、テストは成功するはずです。 - クライアント側では、大きなリクエストボディを送信し、レスポンスボディが期待通りに「hi」であることを確認します。これにより、
ReadFrom
の動作順序が正しく、データが完全に転送されることが検証されます。
関連リンク
- GitHub Issue: https://github.com/golang/go/issues/5660
- Go CL (Code Review): https://golang.org/cl/12632043
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEQ_tEJf9aGOGIW3VaUzzeRH5JEAyAli9hUwZTYjmEF157yeBsft3ac_gz5xY58tx2gYK5vjpdsJRLo_mOxH95ZuZZYkQdXrfEweURAEFIRPe2TICmcI_hzDj9BIkqvCvCb7sP
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFK1K7DgPkn2OeGH8vE51tW1dG4JyIXNx2AWD3d8FY-ReeMQ4QL1zr3nidZoToHoYmjORDAWxTmgG_PYSJklWIjqfffb1dgv2J0vBnaZ9SbvJ_psL50tUOrYWs92E7AQAc=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHzth0EHU6PkV-LcZLZ8pbm0K9IPE07X49rpYeKDzTSKBK6-Ihd90KcCMz6vqT_6fVqgRZ3tLf5DtKY7f36UPL26VbmOmEdj9nk_G9vPez-6DAv5q-BfUehfZ6E4Vs4apTWoIYLg5Wf
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEk-ltMLyNJa8Ux54kEbqIEtkcWA3d32QcvG5oB2HaLB3iswzJMcpjeBdOk0gbsaIOdkCDcqkKE62v9VFYczR0NifZBzkQFDR3Stgpl0U58UhfZnR7ZoT3NLED2DRdIFxsjOl3u