[インデックス 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-Type
やContent-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-Since
やIf-None-Match
ヘッダを使用)を送信し、リソースが変更されていないことを示します。このレスポンスにはメッセージボディを含めてはなりません。これはキャッシュの効率化に非常に重要です。
- 例:
- 4xx (Client Error): クライアントが誤ったリクエストを送信したことを示します。
- 例:
400 Bad Request
、404 Not Found
。
- 例:
- 5xx (Server Error): サーバーがリクエストの処理に失敗したことを示します。
- 例:
500 Internal Server Error
、503 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 Content
や1xx
系の情報提供ステータスコードも同様にボディを許可しません。このコミットは、これらのステータスコードすべてに対して、既存のヘルパー関数であるbodyAllowedForStatus
を適用することで、この挙動を統一しています。
bodyAllowedForStatus
関数は、与えられたHTTPステータスコードがレスポンスボディを許可するかどうかを判定します。この関数がfalse
を返す場合(つまり、ボディが許可されないステータスコードの場合)、net/http
パッケージは以下の処理を行います。
Content-Length
ヘッダの抑制: ボディが許可されないステータスコードの場合、Content-Length
ヘッダは送信されません。これは、ボディが存在しないため、その長さを伝える必要がないためです。Content-Type
ヘッダおよびTransfer-Encoding
ヘッダの抑制: RFC 2616のセクション10.3.5 (304 Not Modifiedに関する記述) には、「レスポンスは他のエンティティヘッダを含んではならない」とあります。この原則を204や1xxにも拡張し、これらのヘッダも送信しないようにします。これにより、クライアントが誤ってボディの存在を期待するのを防ぎます。ResponseWriter.Write
呼び出しの無視: ハンドラがResponseWriter.Write
を呼び出してボディを書き込もうとしても、これらのステータスコードが設定されている場合は、その書き込みは無視されます。これにより、開発者が誤ってボディを書き込もうとした場合でも、プロトコル違反を防ぎます。
テストケースの拡充も重要な変更点です。TestNoContentTypeOnNotModified
という既存のテスト関数がTestCodesPreventingContentTypeAndBody
にリネームされ、StatusNotModified
だけでなく、StatusNoContent
やStatusContinue
(1xxの例) も含めて、これらのステータスコードでContent-Length
ヘッダやレスポンスボディが送信されないことを検証するようになりました。これにより、将来的に同様のバグが再発するのを防ぐための安全網が強化されています。
この変更は、Goのnet/http
パッケージがHTTPプロトコル仕様に厳密に準拠し、より予測可能で堅牢な挙動を提供する上で重要な改善です。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/net/http/serve_test.go
: HTTPレスポンスの挙動をテストするファイル。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
を返します。
変更のポイント
-
Content-Length
ヘッダの制御:server.go
のwriteHeader
関数内で、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 != StatusNotModified
がbodyAllowedForStatus(w.status)
に置き換えられたことで、bodyAllowedForStatus
がfalse
を返す(つまりボディが許可されない)すべてのステータスコード(1xx, 204, 304)に対して、Content-Length
ヘッダが自動的に抑制されるようになりました。 -
エンティティヘッダの削除:
writeHeader
関数内には、特定のステータスコードの場合にContent-Type
、Content-Length
、Transfer-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に関する規定を他のボディを許可しないステータスコードにも適用するという意図を反映しています。 -
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
が呼び出された際に、bodyAllowedForStatus
がfalse
を返すステータスコード(1xx, 204, 304)が設定されている場合は、実際にボディが書き込まれないようになります。これにより、ハンドラが誤ってボディを書き込もうとしても、プロトコル違反を防ぐことができます。
これらの変更により、Goのnet/http
パッケージはHTTPプロトコル仕様にさらに厳密に準拠し、開発者が意図せずプロトコル違反のレスポンスを生成してしまうリスクを低減します。
関連リンク
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - RFC 7231 (Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content): https://datatracker.ietf.org/doc/html/rfc7231
- RFC 9110 (HTTP Semantics): https://datatracker.ietf.org/doc/html/rfc9110 (RFC 7231の後継)
参考にした情報源リンク
- HTTP 204 No Content: https://developer.mozilla.org/ja/docs/Web/HTTP/Status/204
- HTTP 304 Not Modified: https://developer.mozilla.org/ja/docs/Web/HTTP/Status/304
- HTTP 1xx (Informational) status codes: https://developer.mozilla.org/ja/docs/Web/HTTP/Status#information_responses
- RFC 2616 Section 10.3.5 (304 Not Modified): https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5