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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージにおけるHTTPレスポンスの挙動に関する修正です。具体的には、HTTPステータスコードが204 (No Content)、304 (Not Modified)、および1xx (Informational) の場合に、レスポンスボディやContent-Typeヘッダの送信を厳密に禁止するように変更されています。これにより、HTTPプロトコルの仕様に準拠し、クライアント側の予期せぬ挙動を防ぐことが目的です。

コミット

commit 36477291cc13313e816cccfcfa62a6bc0ac43d15
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Jan 16 11:43:52 2014 -0800

    net/http: don't allow Content-Type or body on 204 and 1xx
    
    Status codes 204, 304, and 1xx don't allow bodies. We already
    had a function for this, but we were hard-coding just 304
    (StatusNotModified) in a few places.  Use the function
    instead, and flesh out tests for all codes.
    
    Fixes #6685
    
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/53290044

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

https://github.com/golang/go/commit/36477291cc13313e816cccfcfa62a6bc0ac43d15

元コミット内容

net/http: don't allow Content-Type or body on 204 and 1xx

ステータスコード204、304、および1xxはボディを許可しません。このための関数は既に存在していましたが、いくつかの場所で304 (StatusNotModified) のみをハードコードしていました。代わりにその関数を使用し、すべてのコードに対するテストを充実させます。

Fixes #6685

変更の背景

HTTPプロトコルには、特定のステータスコードにおいてレスポンスボディや特定のヘッダ(特にContent-TypeContent-Length)の送信が厳しく制限されているという明確な仕様があります。

  • 204 No Content: リクエストは成功したが、返すコンテンツがないことを示します。このレスポンスにはメッセージボディを含めてはなりません。
  • 304 Not Modified: クライアントが条件付きGETリクエストを送信し、リソースが変更されていないことを示します。このレスポンスにもメッセージボディを含めてはなりません。
  • 1xx Informational: リクエストが受信され、処理が継続中であることを示す情報提供のレスポンスです。これらのレスポンスにもメッセージボディを含めてはなりません。

以前のGoのnet/httpパッケージでは、304 (StatusNotModified) についてはボディを送信しないように一部で考慮されていましたが、204 (No Content) や1xx (Informational) のステータスコードについては、この制約が十分に適用されていませんでした。また、304の処理も一貫性がなく、特定の場所でハードコードされていました。

この不整合は、クライアント側で予期せぬ挙動を引き起こす可能性がありました。例えば、ボディが許可されないはずの204レスポンスに誤ってボディが含まれてしまうと、クライアントがそのボディを処理しようとしてエラーになったり、セキュリティ上の問題が発生したりする恐れがあります。

このコミットは、これらのHTTPプロトコル仕様への準拠を徹底し、net/httpパッケージの堅牢性と信頼性を向上させることを目的としています。特に、既存のbodyAllowedForStatus関数をより広範に適用し、関連するテストケースを拡充することで、将来的な回帰を防ぐ意図があります。

Fixes #6685という記述がありますが、Goの公式Issueトラッカーで直接#6685という番号のIssueは見つかりませんでした。これは、内部的なトラッキング番号であるか、あるいはIssueが統合・変更された可能性が考えられます。しかし、この記述は通常、報告されたバグや機能要求に対応するものであることを示唆しています。

前提知識の解説

HTTPステータスコードの分類と特性

HTTPステータスコードは、HTTPリクエストの結果を示す3桁の数字です。これらは大きく5つのクラスに分類されます。

  • 1xx (Informational): リクエストが受信され、処理が継続中であることを示す情報提供のレスポンス。
    • 例: 100 Continue (クライアントはリクエストの残りを送信すべき)、101 Switching Protocols (サーバーはプロトコルを切り替えることを承諾した)。
    • 重要な特性: RFC 7231 (および後継のRFC 9110) によると、1xxレスポンスはメッセージボディやトレーラーを含んではなりません。ステータスラインとオプションのヘッダのみで構成され、空行で終了します。
  • 2xx (Success): リクエストが正常に受信、理解、受理されたことを示します。
    • 例: 200 OK (リクエスト成功)、201 Created (リソースが作成された)。
    • 204 No Content: リクエストは成功したが、返すコンテンツがないことを示します。このレスポンスにはメッセージボディを含めてはなりません。これは、例えばフォームの送信後やDELETEリクエストの成功時など、クライアントが現在のビューを更新する必要がない場合によく使用されます。
  • 3xx (Redirection): リクエストを完了するために、さらなるアクションが必要であることを示します。
    • 例: 301 Moved Permanently (リソースが恒久的に移動した)、302 Found (リソースが一時的に移動した)。
    • 304 Not Modified: クライアントが条件付きGETリクエスト(If-Modified-SinceIf-None-Matchヘッダを使用)を送信し、リソースが変更されていないことを示します。このレスポンスにはメッセージボディを含めてはなりません。これはキャッシュの効率化に非常に重要です。
  • 4xx (Client Error): クライアントが誤ったリクエストを送信したことを示します。
    • 例: 400 Bad Request404 Not Found
  • 5xx (Server Error): サーバーがリクエストの処理に失敗したことを示します。
    • 例: 500 Internal Server Error503 Service Unavailable

HTTPヘッダ

  • Content-Type: レスポンスボディのメディアタイプ(MIMEタイプ)を示します。例: text/html; charset=utf-8。ボディがないレスポンスでは通常不要です。
  • Content-Length: レスポンスボディのバイト単位のサイズを示します。ボディがないレスポンスでは通常0または省略されます。

Go言語のnet/httpパッケージ

Go言語のnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。

  • http.ResponseWriter: HTTPレスポンスを構築するためのインターフェース。WriteHeaderメソッドでステータスコードを設定し、Writeメソッドでボディを書き込みます。
  • http.Request: 受信したHTTPリクエストを表す構造体。
  • http.HandlerFunc: HTTPリクエストを処理するための関数型。

このコミットでは、http.ResponseWriterの実装内部で、特定のステータスコードが設定された場合に、Content-Typeやボディの書き込みを抑制するロジックが強化されています。

技術的詳細

このコミットの核心は、HTTPプロトコル仕様における「特定のステータスコードではレスポンスボディを許可しない」というルールを、Goのnet/httpパッケージ内でより厳密かつ一貫して適用することにあります。

以前の実装では、304 Not Modifiedステータスコードの場合にボディを許可しないというロジックが部分的にハードコードされていました。しかし、HTTP仕様では204 No Content1xx系の情報提供ステータスコードも同様にボディを許可しません。このコミットは、これらのステータスコードすべてに対して、既存のヘルパー関数であるbodyAllowedForStatusを適用することで、この挙動を統一しています。

bodyAllowedForStatus関数は、与えられたHTTPステータスコードがレスポンスボディを許可するかどうかを判定します。この関数がfalseを返す場合(つまり、ボディが許可されないステータスコードの場合)、net/httpパッケージは以下の処理を行います。

  1. Content-Lengthヘッダの抑制: ボディが許可されないステータスコードの場合、Content-Lengthヘッダは送信されません。これは、ボディが存在しないため、その長さを伝える必要がないためです。
  2. Content-TypeヘッダおよびTransfer-Encodingヘッダの抑制: RFC 2616のセクション10.3.5 (304 Not Modifiedに関する記述) には、「レスポンスは他のエンティティヘッダを含んではならない」とあります。この原則を204や1xxにも拡張し、これらのヘッダも送信しないようにします。これにより、クライアントが誤ってボディの存在を期待するのを防ぎます。
  3. ResponseWriter.Write呼び出しの無視: ハンドラがResponseWriter.Writeを呼び出してボディを書き込もうとしても、これらのステータスコードが設定されている場合は、その書き込みは無視されます。これにより、開発者が誤ってボディを書き込もうとした場合でも、プロトコル違反を防ぎます。

テストケースの拡充も重要な変更点です。TestNoContentTypeOnNotModifiedという既存のテスト関数がTestCodesPreventingContentTypeAndBodyにリネームされ、StatusNotModifiedだけでなく、StatusNoContentStatusContinue (1xxの例) も含めて、これらのステータスコードでContent-Lengthヘッダやレスポンスボディが送信されないことを検証するようになりました。これにより、将来的に同様のバグが再発するのを防ぐための安全網が強化されています。

この変更は、Goのnet/httpパッケージがHTTPプロトコル仕様に厳密に準拠し、より予測可能で堅牢な挙動を提供する上で重要な改善です。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/net/http/serve_test.go: HTTPレスポンスの挙動をテストするファイル。
  2. src/pkg/net/http/server.go: HTTPサーバーのコアロジックを実装するファイル。

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

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2062,30 +2062,35 @@ func TestServerReaderFromOrder(t *testing.T) {
 	}\n }\n \n-// Issue 6157\n-func TestNoContentTypeOnNotModified(t *testing.T) {\n-\tht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {\n-\t\tif r.URL.Path == \"/header\" {\n-\t\t\tw.Header().Set(\"Content-Length\", \"123\")\n-\t\t}\n-\t\tw.WriteHeader(StatusNotModified)\n-\t\tif r.URL.Path == \"/more\" {\n-\t\t\tw.Write([]byte(\"stuff\"))\n-\t\t}\n-\t}))\n-\tfor _, req := range []string{\n-\t\t\"GET / HTTP/1.0\",\n-\t\t\"GET /header HTTP/1.0\",\n-\t\t\"GET /more HTTP/1.0\",\n-\t\t\"GET / HTTP/1.1\",\n-\t\t\"GET /header HTTP/1.1\",\n-\t\t\"GET /more HTTP/1.1\",\n-\t} {\n-\t\tgot := ht.rawResponse(req)\n-\t\tif !strings.Contains(got, \"304 Not Modified\") {\n-\t\t\tt.Errorf(\"Non-304 Not Modified for %q: %s\", req, got)\n-\t\t} else if strings.Contains(got, \"Content-Length\") {\n-\t\t\tt.Errorf(\"Got a Content-Length from %q: %s\", req, got)\n+// Issue 6157, Issue 6685\n+func TestCodesPreventingContentTypeAndBody(t *testing.T) {\n+\tfor _, code := range []int{StatusNotModified, StatusNoContent, StatusContinue} {\n+\t\tht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\t\tif r.URL.Path == \"/header\" {\n+\t\t\t\tw.Header().Set(\"Content-Length\", \"123\")\n+\t\t\t}\n+\t\t\tw.WriteHeader(code)\n+\t\t\tif r.URL.Path == \"/more\" {\n+\t\t\t\tw.Write([]byte(\"stuff\"))\n+\t\t\t}\n+\t\t}))\n+\t\tfor _, req := range []string{\n+\t\t\t\"GET / HTTP/1.0\",\n+\t\t\t\"GET /header HTTP/1.0\",\n+\t\t\t\"GET /more HTTP/1.0\",\n+\t\t\t\"GET / HTTP/1.1\",\n+\t\t\t\"GET /header HTTP/1.1\",\n+\t\t\t\"GET /more HTTP/1.1\",\n+\t\t} {\n+\t\t\tgot := ht.rawResponse(req)\n+\t\t\twantStatus := fmt.Sprintf(\"%d %s\", code, StatusText(code))\n+\t\t\tif !strings.Contains(got, wantStatus) {\n+\t\t\t\tt.Errorf(\"Code %d: Wanted %q Modified for %q: %s\", code, req, got)\n+\t\t\t} else if strings.Contains(got, \"Content-Length\") {\n+\t\t\t\tt.Errorf(\"Code %d: Got a Content-Length from %q: %s\", code, req, got)\n+\t\t\t} else if strings.Contains(got, \"stuff\") {\n+\t\t\t\tt.Errorf(\"Code %d: Response contains a body from %q: %s\", code, req, got)\n+\t\t\t}\n \t\t}\n \t}\n }\n```

*   `TestNoContentTypeOnNotModified`関数が`TestCodesPreventingContentTypeAndBody`にリネームされました。
*   テスト対象のステータスコードが`StatusNotModified`だけでなく、`StatusNoContent` (204) と`StatusContinue` (100) も含むようにループが追加されました。
*   各ステータスコードに対して、`Content-Length`ヘッダが含まれていないこと、およびレスポンスボディ("stuff"という文字列)が含まれていないことを検証するアサーションが追加されました。

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

```diff
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -735,7 +735,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 	// response header and this is our first (and last) write, set
 	// it, even to zero. This helps HTTP/1.0 clients keep their
 	// "keep-alive" connections alive.\n-\t// Exceptions: 304 responses never get Content-Length, and if\n+\t// Exceptions: 304/204/1xx responses never get Content-Length, and if\n \t// it was a HEAD request, we don't know the difference between
 	// 0 actual bytes and 0 bytes because the handler noticed it
 	// was a HEAD request and chose not to write anything.  So for
 	// write non-zero bytes.  If it's actually 0 bytes and the
@@ -743,7 +743,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 	// handler never looked at the Request.Method, we just don't
 	// send a Content-Length header.\n-\tif w.handlerDone && w.status != StatusNotModified && header.get(\"Content-Length\") == \"\" && (!isHEAD || len(p) > 0) {\n+\tif w.handlerDone && bodyAllowedForStatus(w.status) && header.get(\"Content-Length\") == \"\" && (!isHEAD || len(p) > 0) {\n \t\tw.contentLength = int64(len(p))\n \t\tsetHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)\n \t}\n@@ -792,7 +792,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 	}\n \n 	code := w.status\n-\tif code == StatusNotModified {\n+\tif !bodyAllowedForStatus(code) {\n \t\t// Must not have body.\n \t\t// RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers"\n \t\tfor _, k := range []string{\"Content-Type\", \"Content-Length\", \"Transfer-Encoding\"} {\n@@ -821,7 +821,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 \t\thasCL = false\n \t}\n \n-\tif w.req.Method == \"HEAD\" || code == StatusNotModified {\n+\tif w.req.Method == \"HEAD\" || !bodyAllowedForStatus(code) {\n \t\t// do nothing\n \t} else if code == StatusNoContent {\n \t\tdelHeader(\"Transfer-Encoding\")\n@@ -915,7 +915,7 @@ func (w *response) bodyAllowed() bool {
 \tif !w.wroteHeader {\n \t\tpanic(\"\")\n \t}\n-\treturn w.status != StatusNotModified\n+\treturn bodyAllowedForStatus(w.status)\n }\n \n // The Life Of A Write is like this:\n```

*   `writeHeader`関数内のコメントが更新され、`304`だけでなく`204/1xx`も`Content-Length`を持たないことが明記されました。
*   `Content-Length`を設定する条件式で、`w.status != StatusNotModified`というハードコードされた条件が`bodyAllowedForStatus(w.status)`に置き換えられました。これにより、ボディが許可されないすべてのステータスコードに対して`Content-Length`が抑制されるようになりました。
*   `Content-Type`, `Content-Length`, `Transfer-Encoding`ヘッダを削除する条件も、`code == StatusNotModified`から`!bodyAllowedForStatus(code)`に変更されました。これにより、ボディが許可されないすべてのステータスコードでこれらのヘッダが削除されるようになりました。
*   `w.req.Method == "HEAD" || code == StatusNotModified`という条件も`w.req.Method == "HEAD" || !bodyAllowedForStatus(code)`に変更され、同様に汎用化されました。
*   `bodyAllowed`メソッドの戻り値も`w.status != StatusNotModified`から`bodyAllowedForStatus(w.status)`に変更され、ボディの書き込みが許可されるかどうかの判定が一貫して`bodyAllowedForStatus`関数に委ねられるようになりました。

## コアとなるコードの解説

このコミットの主要な変更は、`src/pkg/net/http/server.go`内の`bodyAllowedForStatus`関数をより広範に利用することで、HTTPプロトコル仕様への準拠を強化している点です。

### `bodyAllowedForStatus` 関数 (変更前後の挙動を理解するための前提)

この関数は、Goの`net/http`パッケージ内部で定義されており、特定のHTTPステータスコードがレスポンスボディを許可するかどうかを判定します。通常、以下のようなロジックを持っています(コミット時点での正確な実装はコードベースを確認する必要がありますが、概念は同じです)。

```go
// bodyAllowedForStatus reports whether a given response status code
// permits a body. See RFC 2616, section 4.3.
func bodyAllowedForStatus(status int) bool {
	switch {
	case status >= 100 && status < 200: // 1xx (Informational)
		return false
	case status == 204: // 204 No Content
		return false
	case status == 304: // 304 Not Modified
		return false
	}
	return true
}

この関数は、1xx、204、304といったステータスコードに対してfalseを返し、それ以外のステータスコードに対してはtrueを返します。

変更のポイント

  1. Content-Lengthヘッダの制御: server.gowriteHeader関数内で、Content-Lengthヘッダを設定するロジックがあります。変更前は、w.status != StatusNotModifiedという条件で304のみを特別扱いしていました。

    // 変更前
    // Exceptions: 304 responses never get Content-Length, and if
    if w.handlerDone && w.status != StatusNotModified && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) {
        // ... Content-Lengthを設定 ...
    }
    
    // 変更後
    // Exceptions: 304/204/1xx responses never get Content-Length, and if
    if w.handlerDone && bodyAllowedForStatus(w.status) && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) {
        // ... Content-Lengthを設定 ...
    }
    

    w.status != StatusNotModifiedbodyAllowedForStatus(w.status)に置き換えられたことで、bodyAllowedForStatusfalseを返す(つまりボディが許可されない)すべてのステータスコード(1xx, 204, 304)に対して、Content-Lengthヘッダが自動的に抑制されるようになりました。

  2. エンティティヘッダの削除: writeHeader関数内には、特定のステータスコードの場合にContent-TypeContent-LengthTransfer-Encodingといったエンティティヘッダを削除するロジックがあります。変更前はここでも304のみが対象でした。

    // 変更前
    code := w.status
    if code == StatusNotModified {
        // Must not have body.
        // RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers"
        for _, k := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} {
            delHeader(k)
        }
    }
    
    // 変更後
    code := w.status
    if !bodyAllowedForStatus(code) { // bodyAllowedForStatusがfalseを返す場合
        // Must not have body.
        // RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers"
        for _, k := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} {
            delHeader(k)
        }
    }
    

    ここでもcode == StatusNotModified!bodyAllowedForStatus(code)に置き換えられ、ボディが許可されないすべてのステータスコードでこれらのヘッダが削除されるようになりました。これは、RFC 2616の304に関する規定を他のボディを許可しないステータスコードにも適用するという意図を反映しています。

  3. ResponseWriter.Writeの挙動: response構造体のbodyAllowed()メソッドは、レスポンスボディの書き込みが許可されているかを判定します。

    // 変更前
    func (w *response) bodyAllowed() bool {
        if !w.wroteHeader {
            panic("")
        }
        return w.status != StatusNotModified
    }
    
    // 変更後
    func (w *response) bodyAllowed() bool {
        if !w.wroteHeader {
            panic("")
        }
        return bodyAllowedForStatus(w.status)
    }
    

    この変更により、ResponseWriter.Writeが呼び出された際に、bodyAllowedForStatusfalseを返すステータスコード(1xx, 204, 304)が設定されている場合は、実際にボディが書き込まれないようになります。これにより、ハンドラが誤ってボディを書き込もうとしても、プロトコル違反を防ぐことができます。

これらの変更により、Goのnet/httpパッケージはHTTPプロトコル仕様にさらに厳密に準拠し、開発者が意図せずプロトコル違反のレスポンスを生成してしまうリスクを低減します。

関連リンク

参考にした情報源リンク