[インデックス 16014] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内の Transport
において、HTTP/1.1 の 100 Continue
レスポンスが予期せず送信された場合に、それを適切に無視し、後続の処理が正常に行われるようにするための修正を導入しています。具体的には、Transport
がレスポンスを読み取る際に、ステータスコードが 100
であればそれをスキップし、次のレスポンスを読み直すロジックが追加されました。これにより、一部のサーバーが不必要に 100 Continue
を送信するケースに対応し、クライアント側の Transport
が「オフバイワンエラー」に陥ることを防ぎます。
コミット
commit a79df7bb20c7c19bebfd35674bf686129d7f079f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Mar 29 20:25:11 2013 -0700
net/http: ignore 100-continue responses in Transport
"There are only two hard problems in computer science:
cache invalidation, naming things, and off-by-one errors."
The HTTP server code already strips Expect: 100-continue on
requests, so httputil.ReverseProxy should be unaffected, but
some servers send unsolicited HTTP/1.1 100 Continue responses,
so we need to skip over them if they're seen to avoid getting
off-by-one on Transport requests/responses.
This does change the behavior of people who were using Client
or Transport directly and explicitly setting "Expect: 100-continue"
themselves, but it didn't work before anyway. Now instead of the
user code seeing a 100 response and then things blowing up, now
it basically works, except the Transport will still blast away
the full request body immediately. That's the part that needs
to be finished to close this issue.
This is the safe quick fix.
Update #3665
R=golang-dev, dsymonds, dave, jgrahamc
CC=golang-dev
https://golang.org/cl/8166045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a79df7bb20c7c19bebfd35674bf686129d7f079f
元コミット内容
このコミットの目的は、net/http
パッケージの Transport
が、予期せぬ HTTP/1.1 100 Continue
レスポンスを適切に処理できるようにすることです。コミットメッセージは、コンピュータサイエンスにおける「2つの難しい問題」という有名なジョークを引用し、off-by-one errors
(オフバイワンエラー) がその一つであることを示唆しています。
GoのHTTPサーバーは既に Expect: 100-continue
ヘッダーをリクエストから取り除くため、httputil.ReverseProxy
は影響を受けません。しかし、一部のサーバーはクライアントが Expect: 100-continue
ヘッダーを送信していないにもかかわらず、自発的に 100 Continue
レスポンスを送信することがあります。このような場合、Transport
はこの予期せぬ 100 Continue
レスポンスを通常のレスポンスとして誤って解釈し、後続の実際のレスポンスとの間で「オフバイワンエラー」を引き起こす可能性がありました。
この修正は、Client
または Transport
を直接使用し、明示的に Expect: 100-continue
を設定していたユーザーの動作を変更する可能性があります。しかし、コミットメッセージによれば、そのようなケースでも以前は正しく機能していなかったため、実質的な問題は少ないとされています。修正後は、ユーザーコードが 100
レスポンスを受け取って処理が中断する代わりに、基本的には機能するようになります。ただし、Transport
は引き続きリクエストボディ全体を即座に送信してしまうため、この点は将来的な改善点として残されています(Issue 2184)。
このコミットは「安全な応急処置 (safe quick fix)」と位置づけられており、Issue #3665 の更新に関連しています。
変更の背景
HTTP/1.1 の 100 Continue
メカニズムは、クライアントが大きなリクエストボディを送信する前に、サーバーがそのリクエストを受け入れる準備ができているかを確認するために使用されます。クライアントは Expect: 100-continue
ヘッダーをリクエストに含め、サーバーはリクエストボディの受信を開始する前に 100 Continue
レスポンスを返します。これにより、サーバーがリクエストを拒否する場合に、クライアントが無駄に大きなボディを送信するのを防ぐことができます。
しかし、このメカニズムは常に厳密に実装されるわけではありません。一部のHTTPサーバーやプロキシは、クライアントが Expect: 100-continue
ヘッダーを送信していないにもかかわらず、誤って 100 Continue
レスポンスを送信することがあります。Goの net/http
パッケージの Transport
は、このような「自発的な (unsolicited)」100 Continue
レスポンスを予期していませんでした。
Transport
は、リクエストを送信した後、サーバーからのレスポンスを読み取ります。もし予期せぬ 100 Continue
レスポンスが最初に到着した場合、Transport
はそれを「実際の」レスポンスとして誤って処理し、その後に続く本来の 200 OK
などのレスポンスを、前のリクエストに対するものとして認識できなくなってしまいます。これが「オフバイワンエラー」であり、HTTP通信の同期が崩れる原因となります。
この問題は、特にプロキシ環境や、HTTP/1.1 の仕様に厳密に従わないサーバーとの通信において顕在化しました。このコミットは、このような状況下でも net/http
クライアントが堅牢に動作するようにするための修正です。
前提知識の解説
HTTP/1.1 100 Continue
HTTP/1.1 の 100 Continue
ステータスコードは、クライアントが大きなリクエストボディを送信する前に、サーバーがそのリクエストのヘッダー部分を受け入れ、ボディの送信を続行してもよいことを示すために使用されます。
通常のフロー:
- クライアントは
Expect: 100-continue
ヘッダーを含むリクエストヘッダーを送信します。 - サーバーはリクエストヘッダーを解析し、ボディの受信準備ができていれば
HTTP/1.1 100 Continue
レスポンスを返します。 - クライアントは
100 Continue
を受け取った後、リクエストボディの送信を開始します。 - サーバーはリクエストボディを受信し、最終的なレスポンス(例:
200 OK
)を返します。
もしサーバーがリクエストヘッダーを受け入れられない場合(例: 認証エラー、不正なヘッダー)、4xx
や 5xx
のエラーレスポンスを直接返し、クライアントはボディを送信せずに済みます。
Go net/http
パッケージ
Go言語の net/http
パッケージは、HTTPクライアントとサーバーを実装するための標準ライブラリです。
http.Client
: HTTPリクエストを送信するための高レベルなインターフェースを提供します。通常、ユーザーはこの構造体を通じてHTTP通信を行います。http.Transport
:http.Client
の内部で使用される低レベルなコンポーネントで、実際のネットワーク接続の確立、リクエストの送信、レスポンスの受信、接続の再利用(Keep-Alive)などを担当します。Transport
は、複数のリクエストに対して単一のTCP接続を再利用することで、パフォーマンスを向上させます。ReadResponse
:net/http
パッケージ内の関数で、bufio.Reader
からHTTPレスポンスを読み取り、*http.Response
構造体にパースします。
オフバイワンエラー (Off-by-one error)
プログラミングにおけるオフバイワンエラーは、ループの境界条件や配列のインデックスなどで、期待される回数よりも1回多く(または少なく)処理が行われたり、期待される範囲よりも1つ多く(または少なく)要素が参照されたりするバグです。このコミットの文脈では、Transport
が予期せぬ 100 Continue
レスポンスを「余分な」レスポンスとして読み取ってしまい、その後の本来のレスポンスの読み取りがずれてしまうことを指します。
技術的詳細
この修正は、net/http/transport.go
ファイル内の (pc *persistConn) readLoop()
メソッドに焦点を当てています。readLoop
は、永続的な接続 (persistConn
) を介してサーバーからのレスポンスを継続的に読み取る役割を担っています。
変更前は、readLoop
は ReadResponse
を呼び出してレスポンスを読み取り、エラーがなければそのレスポンスを処理していました。しかし、サーバーが 100 Continue
を自発的に送信した場合、ReadResponse
はこの 100 Continue
を有効なレスポンスとしてパースしてしまいます。readLoop
はこの 100 Continue
を処理した後、次のレスポンスを待機しますが、サーバーは既に 100 Continue
を送信済みであるため、次に送信されるのは本来の最終レスポンスです。これにより、Transport
の内部状態と、サーバーが送信するレスポンスのシーケンスとの間に不整合が生じ、結果として「オフバイワンエラー」が発生していました。
このコミットでは、ReadResponse
が返したレスポンスの StatusCode
が 100
である場合、そのレスポンスを破棄し、再度 ReadResponse
を呼び出して次のレスポンスを読み取るように変更されています。これにより、予期せぬ 100 Continue
レスポンスが透過的にスキップされ、Transport
は本来の最終レスポンスを正しく受け取ることができるようになります。
コミットメッセージでは、この修正が「安全な応急処置」であると述べられています。これは、Expect: 100-continue
ヘッダーを明示的に設定したリクエストに対して、Transport
がリクエストボディの送信を一時停止し、100 Continue
を待ってから送信を再開するという、より洗練された 100 Continue
のハンドリングがまだ実装されていないためです(Issue 2184)。現在の修正では、100 Continue
が返されても、Transport
はリクエストボディを即座に「ブラストアウェイ(一気に送信)」してしまいます。しかし、予期せぬ 100 Continue
によるオフバイワンエラーを防ぐという点では、この修正は効果的です。
テストケース TestTransportReading100Continue
は、この修正の動作を検証するために追加されました。このテストは、カスタムの Dial
関数を持つ Transport
を設定し、サーバー側が意図的に 100 Continue
とそれに続く 200 OK
レスポンスを送信するシナリオをシミュレートします。クライアントが複数のリクエストを送信し、それぞれのレスポンスが正しく 200 OK
として受信されることを確認することで、100 Continue
が適切に無視されていることを検証しています。
コアとなるコードの変更箇所
src/pkg/net/http/transport.go
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -686,6 +686,14 @@ func (pc *persistConn) readLoop() {
var resp *Response
if err == nil {
resp, err = ReadResponse(pc.br, rc.req)
+ if err == nil && resp.StatusCode == 100 {
+ // Skip any 100-continue for now.
+ // TODO(bradfitz): if rc.req had "Expect: 100-continue",
+ // actually block the request body write and signal the
+ // writeLoop now to begin sending it. (Issue 2184) For now we
+ // eat it, since we're never expecting one.
+ resp, err = ReadResponse(pc.br, rc.req)
+ }
}
hasBody := resp != nil && rc.req.Method != "HEAD" && resp.ContentLength != 0
src/pkg/net/http/transport_test.go
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -7,6 +7,7 @@
package http_test
import (
+ "bufio"
"bytes"
"compress/gzip"
"crypto/rand"
@@ -1399,6 +1400,99 @@ func TestTransportSocketLateBinding(t *testing.T) {
dialGate <- true
}
+// Issue 2184
+func TestTransportReading100Continue(t *testing.T) {
+ defer afterTest(t)
+
+ var writers struct {
+ sync.Mutex
+ list []*io.PipeWriter
+ }
+ registerPipe := func(pw *io.PipeWriter) {
+ writers.Lock()
+ defer writers.Unlock()
+ writers.list = append(writers.list, pw)
+ }
+ defer func() {
+ writers.Lock()
+ defer writers.Unlock()
+ for _, pw := range writers.list {
+ pw.Close()
+ }
+ }()
+
+ const numReqs = 5
+ reqBody := func(n int) string { return fmt.Sprintf("request body %d", n) }
+ reqID := func(n int) string { return fmt.Sprintf("REQ-ID-%d", n) }
+
+ send100Response := func(w *io.PipeWriter, r *io.PipeReader) {
+ defer w.Close()
+ defer r.Close()
+ br := bufio.NewReader(r)
+ n := 0
+ for {
+ n++
+ req, err := ReadRequest(br)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ slurp, err := ioutil.ReadAll(req.Body)
+ if err != nil || string(slurp) != reqBody(n) {
+ t.Errorf("Server got %q, %v; want 'body'", slurp, err)
+ return
+ }
+ id := req.Header.Get("Request-Id")
+ body := fmt.Sprintf("Response number %d", n)
+ v := []byte(strings.Replace(fmt.Sprintf(`HTTP/1.1 100 Continue
+Date: Thu, 28 Feb 2013 17:55:41 GMT
+
+HTTP/1.1 200 OK
+Content-Type: text/html
+Echo-Request-Id: %s
+Content-Length: %d
+
+%s`, id, len(body), body), "\n", "\r\n", -1))
+ w.Write(v)
+ if id == reqID(numReqs) {
+ return
+ }
+ }
+
+ }
+
+ tr := &Transport{
+ Dial: func(n, addr string) (net.Conn, error) {
+ pr, pw := io.Pipe()
+ registerPipe(pw)
+ conn := &rwTestConn{
+ Reader: pr,
+ Writer: pw,
+ }
+ go send100Response(pw, pr)
+ return conn, nil
+ },
+ DisableKeepAlives: false,
+ }
+ defer tr.CloseIdleConnections()
+ c := &Client{Transport: tr}
+ for i := 1; i <= numReqs; i++ {
+ req, _ := NewRequest("POST", "http://dummy.tld/", strings.NewReader(reqBody(i)))
+ req.Header.Set("Request-Id", reqID(i))
+ res, err := c.Do(req)
+ if err != nil {
+ t.Fatalf("Do (i=%d): %v", i, err)
+ }
+ if res.StatusCode != 200 {
+ t.Fatalf("Response Statuscode=%d; want 200 (i=%d): %v", res.StatusCode, i, err)
+ }
+ _, err = ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("Slurp error (i=%d): %v", i, err)
+ }
+ }
+}
+
type proxyFromEnvTest struct {
req string // URL to fetch; blank means "http://example.com"
env string
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更
readLoop
関数は、persistConn
(永続的な接続) からのレスポンスを読み取る主要なループです。
var resp *Response
if err == nil {
resp, err = ReadResponse(pc.br, rc.req)
if err == nil && resp.StatusCode == 100 {
// Skip any 100-continue for now.
// TODO(bradfitz): if rc.req had "Expect: 100-continue",
// actually block the request body write and signal the
// writeLoop now to begin sending it. (Issue 2184) For now we
// eat it, since we're never expecting one.
resp, err = ReadResponse(pc.br, rc.req)
}
}
このコードブロックは、ReadResponse
が正常にレスポンスを読み取り、かつそのレスポンスの StatusCode
が 100
である場合に実行されます。
resp, err = ReadResponse(pc.br, rc.req)
: 最初にレスポンスを読み取ります。if err == nil && resp.StatusCode == 100
: 読み取りが成功し、かつステータスコードが100
(Continue) であるかをチェックします。// Skip any 100-continue for now.
: コメントは、これが100 Continue
をスキップするためのものであることを明確に示しています。// TODO(bradfitz): ... (Issue 2184)
: これは、将来的な改善点として、Expect: 100-continue
ヘッダーがリクエストに含まれていた場合に、リクエストボディの送信を一時停止し、100 Continue
を待ってから送信を再開するという、より完全な100 Continue
のハンドリングが必要であることを示しています。現在の修正は、あくまで「予期せぬ100 Continue
」を処理するためのものです。resp, err = ReadResponse(pc.br, rc.req)
: 重要なのはこの行です。100 Continue
レスポンスを読み取った後、それを破棄し、再度ReadResponse
を呼び出して次のレスポンスを読み取ろうとします。これにより、100 Continue
があたかも存在しなかったかのように扱われ、Transport
は本来の最終レスポンスを正しく処理できるようになります。
src/pkg/net/http/transport_test.go
の追加
TestTransportReading100Continue
テスト関数は、この修正が正しく機能することを確認するために追加されました。
send100Response
関数: この関数は、テスト用の「サーバー」として機能します。クライアントからのリクエストを受信した後、意図的にHTTP/1.1 100 Continue
レスポンスと、それに続くHTTP/1.1 200 OK
レスポンスを送信します。これにより、Transport
が予期せぬ100 Continue
を受け取るシナリオをシミュレートします。
この部分で、v := []byte(strings.Replace(fmt.Sprintf(`HTTP/1.1 100 Continue Date: Thu, 28 Feb 2013 17:55:41 GMT HTTP/1.1 200 OK Content-Type: text/html Echo-Request-Id: %s Content-Length: %d %s`, id, len(body), body), "\n", "\r\n", -1)) w.Write(v)
100 Continue
と200 OK
の両方が単一のバイトストリームとして書き込まれ、クライアントに送信されます。- カスタム
Transport.Dial
: テストでは、http.Transport
のDial
フィールドをカスタム関数に設定しています。このカスタムDial
関数は、実際のネットワーク接続の代わりにio.Pipe
を使用して、クライアントとsend100Response
関数(サーバー側)の間でデータをやり取りします。これにより、ネットワークを介さずに、テスト内でサーバーの動作を完全に制御できます。 - クライアントリクエストと検証: テストは
http.Client
を使用して複数のPOSTリクエストを送信し、それぞれのレスポンスのステータスコードが200 OK
であること、そしてエラーが発生しないことを検証します。これにより、Transport
が100 Continue
を透過的に処理し、最終的な200 OK
レスポンスを正しく返すことを確認しています。
このテストは、100 Continue
レスポンスが Transport
によって正しく「消費」され、実際のレスポンスの読み取りに影響を与えないことを保証するものです。
関連リンク
- Go Issue 2184:
net/http: support Expect: 100-continue
- このコミットで言及されている、より完全な100 Continue
サポートに関するIssue。 - Go Issue 3665:
net/http: Transport gets off-by-one with unsolicited 100-continue
- このコミットが解決しようとしている、自発的な100 Continue
によるオフバイワンエラーに関するIssue。 - Go CL 8166045:
net/http: ignore 100-continue responses in Transport
- このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリスト。
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 8.2.3 100 Continue)
- Go
net/http
パッケージドキュメント - Go
net/http/httputil
パッケージドキュメント (ReverseProxyについて) - "There are only two hard problems in computer science" - Wikipedia