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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージ内のHTTPサーバーの実装に関する変更を含んでいます。具体的には、src/pkg/net/http/server.goがサーバーのコアロジックを、src/pkg/net/http/serve_test.goが関連するテストケースを扱っています。変更の目的は、Expect: 100-continueヘッダを持つリクエストの処理を改善し、特にボディが0バイトの場合の挙動を修正することです。

コミット

commit 10a273196b5aed2d72d50573bfc5c0bdb2e631a2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Apr 10 22:25:31 2014 -0700

    net/http: don't reject 0-lengthed bodies with Expect 100-continue
    
    I was implementing rules from RFC 2616. The rules are apparently useless,
    ambiguous, and too strict for common software on the Internet. (e.g. curl)
    
    Add more tests, including a test of a chunked request.
    
    Fixes #7625
    
    LGTM=dsymonds
    R=golang-codereviews, dsymonds
    CC=adg, golang-codereviews, rsc
    https://golang.org/cl/84480045

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

https://github.com/golang/go/commit/10a273196b5aed2d72d50573bfc5c0bdb2e631a2

元コミット内容

net/http: don't reject 0-lengthed bodies with Expect 100-continue

I was implementing rules from RFC 2616. The rules are apparently useless,
ambiguous, and too strict for common software on the Internet. (e.g. curl)

Add more tests, including a test of a chunked request.

Fixes #7625

変更の背景

このコミットは、Goのnet/httpパッケージがExpect: 100-continueヘッダを持つHTTPリクエストを処理する際の、特定の挙動を修正するために行われました。以前の実装では、HTTP/1.1の仕様であるRFC 2616の解釈に基づき、Content-Length: 0(つまりボディが空)であるにもかかわらずExpect: 100-continueヘッダが指定されたリクエストを400 Bad Requestとして拒否していました。

コミットメッセージによると、このRFC 2616のルールは「役に立たず、曖昧で、インターネット上の一般的なソフトウェア(例: curl)にとっては厳しすぎる」と判断されました。実際、多くのHTTPクライアントやプロキシは、ボディが空であってもExpect: 100-continueを送信する場合があります。このような厳格な解釈は、GoのHTTPサーバーがこれらの一般的なクライアントと相互運用する際に問題を引き起こしていました。

具体的には、Issue #7625で報告された問題に対応しています。この問題は、Content-Length: 0のリクエストに対してExpect: 100-continueヘッダが存在する場合に、サーバーが不適切に400 Bad Requestを返すというものでした。この挙動は、特にWebDAVのようなプロトコルで、ボディを持たないPROPFINDなどのリクエストでExpect: 100-continueが使用される場合に問題となります。

この変更は、より現実的なインターネットの利用状況に合わせ、GoのHTTPサーバーの堅牢性と互換性を向上させることを目的としています。

前提知識の解説

HTTP/1.1 Expect: 100-continue ヘッダ

Expect: 100-continueはHTTP/1.1で導入されたリクエストヘッダです。クライアントが大きなリクエストボディを送信する前に、サーバーがそのリクエストを受け入れる準備ができているかを確認するために使用されます。

通常のHTTPリクエストでは、クライアントはヘッダとボディを一度に送信します。しかし、ボディが非常に大きい場合、サーバーがヘッダを処理した結果、リクエストを拒否する可能性があるにもかかわらず、クライアントが大量のボディデータを無駄に送信してしまう可能性があります。

Expect: 100-continueヘッダを使用すると、クライアントはまずヘッダのみを送信し、サーバーからの100 Continueレスポンスを待ちます。

  • サーバーが100 Continueを返した場合、クライアントは安心してボディの送信を開始します。
  • サーバーが4xx(クライアントエラー)や5xx(サーバーエラー)のステータスコードを返した場合、クライアントはボディを送信せずにリクエストを中止できます。これにより、帯域幅の無駄を避けることができます。

RFC 2616

RFC 2616は、HTTP/1.1プロトコルを定義する主要な仕様の一つです。このRFCは、HTTPのメッセージフォーマット、メソッド、ステータスコード、ヘッダフィールドなど、プロトコルのあらゆる側面を詳細に記述しています。しかし、インターネットの進化とともに、一部のルールが現実の利用状況と乖離したり、解釈が曖昧であったりすることが判明し、後続のRFC(例: RFC 7230-7235)で更新・廃止されています。このコミットで言及されている「曖昧で厳しすぎるルール」は、まさにそのようなRFC 2616の課題を指しています。

HTTP ステータスコード

  • 100 Continue: クライアントがリクエストボディを送信し続けるべきであることを示す情報レスポンス。Expect: 100-continueヘッダに対するサーバーの肯定的な応答。
  • 200 OK: リクエストが成功したことを示す標準的なレスポンス。
  • 400 Bad Request: サーバーがクライアントエラー(例: 不正な構文、無効なリクエストメッセージフレーミング)のためにリクエストを理解または処理できないことを示す。
  • 401 Unauthorized: 認証が必要なリソースへのアクセスが拒否されたことを示す。クライアントは認証情報を提供する必要がある。
  • 417 Expectation Failed: Expectリクエストヘッダで指定された期待値がサーバーによって満たされなかったことを示す。

Chunked Transfer Encoding

Chunked Transfer Encodingは、HTTP/1.1で導入された転送エンコーディングメカニズムの一つです。これは、サーバーがレスポンスボディの全長を事前に知らなくても、動的にコンテンツを生成して送信できる場合に特に有用です。

通常のHTTPレスポンスでは、Content-Lengthヘッダを使用してボディのサイズを事前に指定します。しかし、チャンク転送エンコーディングを使用する場合、ボディは一連の「チャンク」に分割され、各チャンクは自身のサイズ情報(16進数)とデータを含みます。ボディの終端は、サイズが0のチャンクで示されます。

このメカニズムは、特にストリーミングデータや、データベースクエリの結果など、コンテンツのサイズが確定するまでに時間がかかる場合に役立ちます。リクエストボディにも適用されることがあり、このコミットのテストケースでその例が追加されています。

技術的詳細

このコミットの核心は、net/http/server.go内のconn.serve()メソッドにおけるExpect: 100-continueヘッダの処理ロジックの変更です。

変更前は、以下の条件で400 Bad Requestを返していました。

if req.ProtoAtLeast(1, 1) {
    // ...
}
if req.ContentLength == 0 {
    w.Header().Set("Connection", "close")
    w.WriteHeader(StatusBadRequest)
    w.finishRequest()
    break
}

このコードは、「HTTP/1.1以上でExpect: 100-continueが指定されており、かつContent-Lengthが0である場合」に、サーバーが400 Bad Requestを返すという厳格なルールを適用していました。これはRFC 2616のセクション8.2.3に記述されている「クライアントがExpect: 100-continueを送信し、かつリクエストボディが空である場合、サーバーは417 Expectation Failedを返すべきではない」という推奨事項を、誤って「400 Bad Requestを返す」と解釈していた可能性があります。しかし、RFC 2616自体がこのシナリオでの具体的なサーバーの挙動について明確な指示を与えていないため、実装によっては解釈が分かれる部分でした。

コミットメッセージが示唆するように、この厳格な解釈は現実世界のHTTPクライアント(特にcurlのような広く使われているツール)との互換性問題を引き起こしていました。多くのクライアントは、ボディが空であってもExpect: 100-continueを送信することがあり、サーバーがこれをBad Requestとして拒否すると、正当なリクエストが処理されなくなります。

このコミットでは、req.ContentLength == 0の場合に400 Bad Requestを返すロジックを削除し、代わりにExpect: 100-continueの処理をreq.ContentLength != 0の場合にのみ適用するように変更しました。

変更後のコード:

if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { // <-- 変更点
    // Wrap the Body reader with one that replies on the connection
    req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}

この変更により、Content-Lengthが0のリクエストに対してExpect: 100-continueヘッダが存在しても、サーバーはもはや400 Bad Requestを返さなくなります。代わりに、ボディがないため100 Continueを送信する必要もなく、通常通りリクエストを処理し、最終的に200 OKなどの適切なステータスコードを返すようになります。これは、RFC 2616の曖昧な部分を、より実用的な「ボディがないなら100 Continueは不要」という解釈で処理することで、幅広いクライアントとの互換性を確保するものです。

また、テストケースの追加も重要な変更点です。特に、Content-Length: 0かつExpect: 100-continueのリクエストが200 OKを返すことを確認するテストや、チャンク転送エンコーディングを使用するリクエストのテストが追加され、新しい挙動が正しく機能することを確認しています。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1158,16 +1158,10 @@ func (c *conn) serve() {
 		// Expect 100 Continue support
 		req := w.req
 		if req.expectsContinue() {
-			if req.ProtoAtLeast(1, 1) {
+			if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
 				// Wrap the Body reader with one that replies on the connection
 				req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
 			}
-			if req.ContentLength == 0 {
-				w.Header().Set("Connection", "close")
-				w.WriteHeader(StatusBadRequest)
-				w.finishRequest()
-				break
-			}
 			req.Header.Del("Expect")
 		} else if req.Header.get("Expect") != "" {
 			w.sendExpectationFailed()

src/pkg/net/http/serve_test.go

テストファイルでは、serverExpectTest構造体にchunkedフィールドが追加され、チャンク転送エンコーディングのテストケースをサポートしています。また、expectTestヘルパー関数が導入され、テストケースの定義が簡潔になっています。

最も重要な変更は、serverExpectTestsスライスに以下のテストケースが追加されたことです。

	// Expect-100 requested but no body (is apparently okay: Issue 7625)
	expectTest(0, "100-continue", true, "200 OK"),
	// Expect-100 requested but handler doesn't read the body
	expectTest(0, "100-continue", false, "401 Unauthorized"),
	// Expect-100 continue with no body, but a chunked body.
	{
		expectation:      "100-continue",
		readBody:         true,
		chunked:          true,
		expectedResponse: "100 Continue",
	},

さらに、テストクライアントのロジックも更新され、Content-Lengthが0の場合のボディ書き込みロジックが修正され、チャンク転送エンコーディングを適切に処理するためのhttputil.NewChunkedWriterの使用が追加されました。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -986,21 +1005,38 @@ func TestServerExpect(t *testing.T) {
 
 		// Only send the body immediately if we're acting like an HTTP client
 		// that doesn't send 100-continue expectations.
-		writeBody := test.contentLength > 0 && strings.ToLower(test.expectation) != "100-continue"
+		writeBody := test.contentLength != 0 && strings.ToLower(test.expectation) != "100-continue"
 
 		go func() {
+			contentLen := fmt.Sprintf("Content-Length: %d", test.contentLength)
+			if test.chunked {
+				contentLen = "Transfer-Encoding: chunked"
+			}
 			_, err := fmt.Fprintf(conn, "POST /?readbody=%v HTTP/1.1\\r\\n"+
 				"Connection: close\\r\\n"+
-				"Content-Length: %d\\r\\n"+
+				"%s\\r\\n"+
 				"Expect: %s\\r\\nHost: foo\\r\\n\\r\\n",
-				test.readBody, test.contentLength, test.expectation)
+				test.readBody, contentLen, test.expectation)
 			if err != nil {
 				t.Errorf("On test %#v, error writing request headers: %v", test, err)
 				return
 			}
 			if writeBody {
+				var targ io.WriteCloser = struct {
+					io.Writer
+					io.Closer
+				}{
+					conn,
+					ioutil.NopCloser(nil),
+				}
+				if test.chunked {
+					targ = httputil.NewChunkedWriter(conn)
+				}
 				body := strings.Repeat("A", test.contentLength)
-				_, err = fmt.Fprint(conn, body)
+				_, err = fmt.Fprint(targ, body)
+				if err == nil {
+					err = targ.Close()
+				}
 				if err != nil {
 					if !test.readBody {
 						// Server likely already hung up on us.

コアとなるコードの解説

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

以前のコードでは、Expect: 100-continueヘッダが存在し、かつContent-Lengthが0である場合に、サーバーは無条件に400 Bad Requestを返していました。これは、RFC 2616の厳格な解釈に基づいていたものの、実際のインターネット上の多くのクライアントの挙動とは合致せず、相互運用性の問題を引き起こしていました。

変更後のコードでは、req.ProtoAtLeast(1, 1)(HTTP/1.1以上であること)に加えて、req.ContentLength != 0という条件が追加されました。

if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
    // Wrap the Body reader with one that replies on the connection
    req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}

この変更により、Expect: 100-continueヘッダが指定されていても、リクエストボディの長さが0である場合は、expectContinueReaderreq.Bodyをラップする処理(つまり、100 Continueレスポンスを送信する準備)は行われなくなります。ボディが存在しないため、クライアントが100 Continueを待つ必要がなく、サーバーもそれを送信する必要がないという、より合理的な挙動になります。これにより、Content-Length: 0のリクエストが不当に拒否されることがなくなりました。

削除されたif req.ContentLength == 0ブロックは、Expect: 100-continueContent-Length: 0の組み合わせを400 Bad Requestとして扱う原因となっていました。このブロックが削除されたことで、サーバーはこの特定のシナリオでエラーを返さなくなり、リクエストを正常に処理できるようになりました。

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

テストファイルでは、新しい挙動を検証するための重要なテストケースが追加されました。

  • expectTest(0, "100-continue", true, "200 OK"): このテストケースは、Content-Length: 0Expect: 100-continueヘッダを持つリクエストが、サーバーによって200 OKとして正常に処理されることを検証します。これは、以前の400 Bad Requestの挙動からの変更を直接的にテストするものです。
  • expectTest(0, "100-continue", false, "401 Unauthorized"): このテストは、Content-Length: 0Expect: 100-continueヘッダを持つリクエストに対して、ハンドラがボディを読み込まない場合に401 Unauthorizedが返されることを確認します。これは、ボディの有無に関わらず、ハンドラのロジックが正しく機能することを示します。
  • チャンク転送エンコーディングのテストケース: chunkedフィールドがserverExpectTest構造体に追加され、チャンク転送エンコーディングのテストが可能になりました。これにより、Expect: 100-continueとチャンク転送エンコーディングの組み合わせが正しく機能することも検証されます。

テストクライアントの変更も重要です。 writeBodyの条件がtest.contentLength > 0からtest.contentLength != 0に変更されたことで、Content-Length: 0のリクエストではボディが送信されないことが明確になりました。 また、チャンク転送エンコーディングをサポートするために、httputil.NewChunkedWriterが導入されました。これにより、テストクライアントはチャンク形式でボディを送信できるようになり、サーバーがチャンク転送エンコーディングのリクエストを正しく処理できるかどうかのテストが可能になりました。

これらのテストの追加と修正により、Expect: 100-continueヘッダの処理がより堅牢になり、特にボディが空のリクエストやチャンク転送エンコーディングを使用するリクエストに対するGoのHTTPサーバーの互換性が向上したことが確認できます。

関連リンク

参考にした情報源リンク