[インデックス 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である場合は、expectContinueReader
でreq.Body
をラップする処理(つまり、100 Continue
レスポンスを送信する準備)は行われなくなります。ボディが存在しないため、クライアントが100 Continue
を待つ必要がなく、サーバーもそれを送信する必要がないという、より合理的な挙動になります。これにより、Content-Length: 0
のリクエストが不当に拒否されることがなくなりました。
削除されたif req.ContentLength == 0
ブロックは、Expect: 100-continue
とContent-Length: 0
の組み合わせを400 Bad Request
として扱う原因となっていました。このブロックが削除されたことで、サーバーはこの特定のシナリオでエラーを返さなくなり、リクエストを正常に処理できるようになりました。
src/pkg/net/http/serve_test.go
の変更
テストファイルでは、新しい挙動を検証するための重要なテストケースが追加されました。
expectTest(0, "100-continue", true, "200 OK")
: このテストケースは、Content-Length: 0
でExpect: 100-continue
ヘッダを持つリクエストが、サーバーによって200 OK
として正常に処理されることを検証します。これは、以前の400 Bad Request
の挙動からの変更を直接的にテストするものです。expectTest(0, "100-continue", false, "401 Unauthorized")
: このテストは、Content-Length: 0
でExpect: 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サーバーの互換性が向上したことが確認できます。
関連リンク
- Go Issue: net/http: Expect: 100-continue with Content-Length: 0 gets 400 Bad Request
- Go CL: https://golang.org/cl/84480045