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

[インデックス 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-SinceIf-None-Match ヘッダーを使用)を送信し、サーバー上のリソースがクライアントのキャッシュにあるバージョン以降変更されていない場合に、サーバーが返すレスポンスです。このレスポンスは、サーバーがリソースの完全な表現を再送信する必要がないことをクライアントに伝えます。

重要な点として、304 Not Modified レスポンスはエンティティボディを持つべきではありません。つまり、レスポンスのペイロード部分にはデータが含まれません。これに伴い、Content-TypeContent-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.gosrc/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つの変更が行われています。

  1. handlerTest 構造体の導入とテストヘルパーの統一: handlerTest という新しい構造体が導入され、HTTPハンドラーのテストをより簡潔に行うためのヘルパーメソッド rawResponse が追加されました。 rawResponse メソッドは、指定されたHTTPリクエスト文字列を受け取り、それを rwTestConn を介して Serve 関数に渡し、ハンドラーからの生のHTTPレスポンス文字列を返します。これにより、テストケースごとに rwTestConnoneConnListener をセットアップする冗長なコードを削減し、テストの可読性と再利用性を向上させています。

  2. 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 の変更は、chunkWriterwriteHeader メソッド内の 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リクエスト文字列を受け取り、内部的に rwTestConnoneConnListener を使用して 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 での修正が期待通りに機能していることが確認されます。

関連リンク

参考にした情報源リンク