[インデックス 17277] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるHTTPレスポンスの挙動に関する修正と、テストヘルパーの改善を含んでいます。
コミット
commit 67a69bce6b0492359f9279b035076fbab12945e0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Aug 15 17:40:05 2013 -0700
net/http: don't send an automatic Content-Length on a 304 Not Modified
Also start of some test helper unification, long overdue.
I refrained from cleaning up the rest in this CL.
Fixes #6157
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/13030043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/67a69bce6b0492359f9279b035076fbab12945e0
元コミット内容
net/http
: 304 Not Modified レスポンスに対して自動的な Content-Length
ヘッダーを送信しないようにする。
また、長らく延期されていたテストヘルパーの統一作業の開始。この変更リストでは残りのクリーンアップは行わない。
Fixes #6157
変更の背景
このコミットの主な目的は、HTTPの仕様に準拠し、304 Not Modified
ステータスコードのレスポンスに Content-Length
ヘッダーが自動的に含まれないようにすることです。
HTTP/1.1の仕様(RFC 2616, 10.3.5 304 Not Modified)では、304 Not Modified
レスポンスはエンティティボディを持つべきではないと明確に規定されています。エンティティボディがない場合、Content-Length
ヘッダーも存在すべきではありません。しかし、Goの net/http
パッケージの以前の実装では、特定の条件下で 304 Not Modified
レスポンスに誤って Content-Length: 0
が含まれてしまう可能性がありました。これは、特にプロキシやキャッシュメカニズムとの相互運用性において問題を引き起こす可能性がありました。
また、コミットメッセージには「テストヘルパーの統一作業の開始」という記述があり、serve_test.go
ファイルの変更からも、テストコードの構造を改善し、より再利用可能で読みやすいテストヘルパーを導入しようとする意図が見て取れます。これは、将来的なテストの追加やメンテナンスを容易にするための基盤作りです。
前提知識の解説
HTTPステータスコード 304 Not Modified
HTTPステータスコード 304 Not Modified
は、クライアントが条件付きリクエスト(例: If-Modified-Since
や If-None-Match
ヘッダーを使用)を送信し、サーバー上のリソースがクライアントのキャッシュにあるバージョン以降変更されていない場合に、サーバーが返すレスポンスです。このレスポンスは、サーバーがリソースの完全な表現を再送信する必要がないことをクライアントに伝えます。
重要な点として、304 Not Modified
レスポンスはエンティティボディを持つべきではありません。つまり、レスポンスのペイロード部分にはデータが含まれません。これに伴い、Content-Type
や Content-Length
といったエンティティボディに関するヘッダーも通常は含まれません。
Content-Length ヘッダー
Content-Length
ヘッダーは、HTTPメッセージのエンティティボディのサイズをオクテット単位で示すヘッダーです。これは、クライアントがレスポンスボディの終わりを正確に判断するために使用されます。304 Not Modified
のようにボディがないレスポンスでは、このヘッダーは不要であり、存在するとHTTPの仕様に反する可能性があります。
Go言語の net/http
パッケージ
net/http
パッケージは、Go言語におけるHTTPクライアントおよびサーバーの実装を提供します。このパッケージは、Webアプリケーションの構築において中心的な役割を果たし、HTTPリクエストの処理、レスポンスの生成、ルーティングなどを担当します。
テスト駆動開発 (TDD) とテストヘルパー
テスト駆動開発(TDD)は、ソフトウェア開発手法の一つで、コードを書く前にテストを記述します。これにより、要件の明確化、設計の改善、バグの早期発見が促進されます。
テストヘルパーは、テストコード内で繰り返し使用される共通のロジックやセットアップ処理をカプセル化するための関数や構造体です。これにより、テストコードの重複を減らし、可読性と保守性を向上させることができます。このコミットでは、handlerTest
という新しいテストヘルパーが導入され、HTTPハンドラーのテストをより簡潔に記述できるようになっています。
技術的詳細
このコミットは、主に src/pkg/net/http/server.go
と src/pkg/net/http/serve_test.go
の2つのファイルに影響を与えています。
src/pkg/net/http/server.go
の変更
server.go
では、chunkWriter
構造体の writeHeader
メソッド内のロジックが変更されています。このメソッドは、HTTPレスポンスヘッダーを書き込む際に、Content-Length
ヘッダーを自動的に設定するかどうかを決定する役割を担っています。
変更前は、w.handlerDone
が真であり、Content-Length
ヘッダーがまだ設定されておらず、かつ HEAD
リクエストでないか、または HEAD
リクエストであってもボディの長さが0より大きい場合に、自動的に Content-Length
が設定されていました。
変更後は、この条件に w.status != StatusNotModified
が追加されました。これにより、レスポンスのステータスコードが 304 Not Modified
である場合には、たとえ他の条件が満たされていても、自動的に Content-Length
ヘッダーが設定されないようになりました。
この修正は、304 Not Modified
レスポンスが Content-Length
ヘッダーを持つべきではないというHTTPの仕様に厳密に準拠するためのものです。
src/pkg/net/http/serve_test.go
の変更
serve_test.go
では、主に以下の2つの変更が行われています。
-
handlerTest
構造体の導入とテストヘルパーの統一:handlerTest
という新しい構造体が導入され、HTTPハンドラーのテストをより簡潔に行うためのヘルパーメソッドrawResponse
が追加されました。rawResponse
メソッドは、指定されたHTTPリクエスト文字列を受け取り、それをrwTestConn
を介してServe
関数に渡し、ハンドラーからの生のHTTPレスポンス文字列を返します。これにより、テストケースごとにrwTestConn
やoneConnListener
をセットアップする冗長なコードを削減し、テストの可読性と再利用性を向上させています。 -
TestNoContentTypeOnNotModified
テストケースの追加:304 Not Modified
レスポンスにContent-Length
ヘッダーが含まれないことを検証するための新しいテストケースTestNoContentTypeOnNotModified
が追加されました。 このテストでは、様々なパス(/
,/header
,/more
)とHTTPバージョン(1.0, 1.1)でリクエストを送信し、ハンドラーがStatusNotModified
を書き込むように設定されています。テストは、返されたレスポンスが304 Not Modified
を含み、かつContent-Length
ヘッダーを含まないことをアサートします。これにより、server.go
で行われた修正が正しく機能していることを保証しています。
コアとなるコードの変更箇所
src/pkg/net/http/server.go
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -737,7 +737,15 @@ 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.
- if w.handlerDone && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) {
+ // Exceptions: 304 responses never get Content-Length, and if
+ // 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
+ // HEAD, the handler should either write the Content-Length or
+ // write non-zero bytes. If it's actually 0 bytes and the
+ // handler never looked at the Request.Method, we just don't
+ // send a Content-Length header.
+ if w.handlerDone && w.status != StatusNotModified && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) {
w.contentLength = int64(len(p))
setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)
}
src/pkg/net/http/serve_test.go
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -122,6 +122,28 @@ func reqBytes(req string) []byte {
return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n")
}
+type handlerTest struct {
+ handler Handler
+}
+
+func newHandlerTest(h Handler) handlerTest {
+ return handlerTest{h}
+}
+
+func (ht handlerTest) rawResponse(req string) string {
+ reqb := reqBytes(req)
+ var output bytes.Buffer
+ conn := &rwTestConn{
+ Reader: bytes.NewReader(reqb),
+ Writer: &output,
+ closec: make(chan bool, 1),
+ }
+ ln := &oneConnListener{conn: conn}
+ go Serve(ln, ht.handler)
+ <-conn.closec
+ return output.String()
+}
+
func TestConsumingBodyOnNextConn(t *testing.T) {
conn := new(testConn)
for i := 0; i < 2; i++ {
@@ -1588,7 +1610,6 @@ func TestOptions(t *testing.T) {
// ones, even if the handler modifies them (~erroneously) after the
// first Write.
func TestHeaderToWire(t *testing.T) {
-\treq := reqBytes("GET / HTTP/1.1\nHost: golang.org")
tests := []struct {
name string
handler func(ResponseWriter, *Request)
@@ -1751,17 +1772,10 @@ func TestHeaderToWire(t *testing.T) {
},
}
for _, tc := range tests {
-\t\tvar output bytes.Buffer
-\t\tconn := &rwTestConn{
-\t\t\tReader: bytes.NewReader(req),\n-\t\t\tWriter: &output,\n-\t\t\tclosec: make(chan bool, 1),\n-\t\t}\n-\t\tln := &oneConnListener{conn: conn}\n-\t\tgo Serve(ln, HandlerFunc(tc.handler))\n-\t\t<-conn.closec
-\t\tif err := tc.check(output.String()); err != nil {\n-\t\t\tt.Errorf("%s: %v\nGot response:\n%s", tc.name, err, output.Bytes())
+\t\tht := newHandlerTest(HandlerFunc(tc.handler))
+\t\tgot := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org")
+\t\tif err := tc.check(got); err != nil {
+\t\t\tt.Errorf("%s: %v\nGot response:\n%s", tc.name, err, got)
\t\t}
}
}
@@ -1952,6 +1966,34 @@ func TestServerReaderFromOrder(t *testing.T) {
}
}
+// Issue 6157
+func TestNoContentTypeOnNotModified(t *testing.T) {
+ ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
+ if r.URL.Path == "/header" {
+ w.Header().Set("Content-Length", "123")
+ }
+ w.WriteHeader(StatusNotModified)
+ if r.URL.Path == "/more" {
+ w.Write([]byte("stuff"))
+ }
+ }))
+ for _, req := range []string{
+ "GET / HTTP/1.0",
+ "GET /header HTTP/1.0",
+ "GET /more HTTP/1.0",
+ "GET / HTTP/1.1",
+ "GET /header HTTP/1.1",
+ "GET /more HTTP/1.1",
+ } {
+ got := ht.rawResponse(req)
+ if !strings.Contains(got, "304 Not Modified") {
+ t.Errorf("Non-304 Not Modified for %q: %s", req, got)
+ } else if strings.Contains(got, "Content-Length") {
+ t.Errorf("Got a Content-Length from %q: %s", req, got)
+ }
+ }
+}
+
func BenchmarkClientServer(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
コアとなるコードの解説
server.go
の変更点
server.go
の変更は、chunkWriter
の writeHeader
メソッド内の if
文の条件式に w.status != StatusNotModified
を追加したことです。
-
変更前:
if w.handlerDone && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0)
この条件は、ハンドラーが処理を完了し(w.handlerDone
)、Content-Length
ヘッダーがまだ設定されておらず(header.get("Content-Length") == ""
)、かつHEAD
リクエストでないか、またはHEAD
リクエストであっても書き込むデータがある場合((!isHEAD || len(p) > 0)
)に、自動的にContent-Length
を設定していました。 -
変更後:
if w.handlerDone && w.status != StatusNotModified && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0)
追加されたw.status != StatusNotModified
は、レスポンスのステータスコードが304 Not Modified
でない場合にのみ、自動的なContent-Length
の設定を許可します。これにより、304 Not Modified
レスポンスでは、たとえボディが空であってもContent-Length: 0
が誤って送信されることを防ぎます。これはHTTP仕様への準拠を強化する重要な変更です。
コメントも追加され、304
レスポンスが Content-Length
を持たないこと、および HEAD
リクエストの場合の Content-Length
の扱いについて明確化されています。
serve_test.go
の変更点
serve_test.go
の変更は、テストの構造化と新しいテストケースの追加に焦点を当てています。
-
handlerTest
構造体とrawResponse
メソッド:handlerTest
は、テスト対象のhttp.Handler
を保持するシンプルな構造体です。rawResponse
メソッドは、HTTPリクエスト文字列を受け取り、内部的にrwTestConn
とoneConnListener
を使用してhttp.Serve
を実行し、ハンドラーからの生のHTTPレスポンス文字列を返します。これにより、テストコード内でHTTPサーバーのセットアップとリクエスト/レスポンスの処理を抽象化し、テストの記述を簡潔にしています。TestHeaderToWire
テストケースでは、このhandlerTest
ヘルパーが導入され、以前の冗長な接続セットアップコードがht.rawResponse(...)
の呼び出しに置き換えられています。 -
TestNoContentTypeOnNotModified
テストケース: この新しいテストケースは、304 Not Modified
レスポンスがContent-Length
ヘッダーを含まないことを検証します。 テストハンドラーは、常にStatusNotModified
を書き込みます。/header
パスの場合には意図的にContent-Length: 123
を設定しようとし、/more
パスの場合にはボディを書き込もうとします。これは、ハンドラーがContent-Length
を設定したり、ボディを書き込んだりしようとした場合でも、net/http
パッケージが304 Not Modified
の場合にはContent-Length
を送信しないことを確認するためです。 テストは、様々なHTTPリクエストに対してrawResponse
を呼び出し、結果のレスポンス文字列が304 Not Modified
を含み、かつContent-Length
を含まないことをstrings.Contains
を用いて検証しています。これにより、server.go
での修正が期待通りに機能していることが確認されます。
関連リンク
- Go Issue 6157:
net/http: don't send an automatic Content-Length on a 304 Not Modified
- https://github.com/golang/go/issues/6157 - Go CL 13030043:
net/http: don't send an automatic Content-Length on a 304 Not Modified
- https://go-review.googlesource.com/c/go/+/13030043
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 10.3.5 304 Not Modified): https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.13 Content-Length): https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13
- Go Documentation - net/http package: https://pkg.go.dev/net/http
- Go Testing: https://go.dev/doc/tutorial/add-a-test
- Test-driven development - Wikipedia: https://en.wikipedia.org/wiki/Test-driven_development