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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HEAD リクエストに対するレスポンスで Content-Length ヘッダが適切に設定されるように修正するものです。これにより、HEAD リクエストのセマンティクスがより正確にHTTP仕様に準拠するようになります。また、関連する TODO コメントの修正も行われています。

コミット

commit 53d091c5ffdcf2f587274e7e97914fe96b183338
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Dec 5 22:36:23 2012 -0800

    net/http: populate ContentLength in HEAD responses
    
    Also fixes a necessary TODO in the process.
    
    Fixes #4126
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6869053

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

https://github.com/golang/go/commit/53d091c5ffdcf2f587274e7e97914fe96b183338

元コミット内容

net/http: HEAD レスポンスで ContentLength を設定する。 この過程で、必要な TODO も修正する。 Fixes #4126

変更の背景

HTTP/1.1 の仕様 (RFC 2616) では、HEAD メソッドは GET メソッドと全く同じヘッダを返すことが求められています。唯一の違いは、HEAD レスポンスにはメッセージボディが含まれないという点です。しかし、Content-Length ヘッダは、もし GET リクエストであった場合に返されるであろうボディの長さを正確に反映する必要があります。

Go言語の net/http パッケージでは、以前は HEAD リクエストに対するレスポンスにおいて Content-Length フィールドが適切に設定されていませんでした。これにより、クライアント側で HEAD レスポンスの Content-Length を利用して、対応する GET リクエストのボディサイズを予測するような処理が正しく機能しない可能性がありました。

このコミットは、この不整合を修正し、HEAD レスポンスにおいても Content-Length が正しく設定されるようにすることで、HTTP仕様への準拠を強化し、クライアント側の互換性と予測可能性を向上させることを目的としています。また、コードベース内に存在していた関連する TODO コメントもこの修正の一環として対応されました。

前提知識の解説

HTTP HEAD メソッド

HTTP HEAD メソッドは、GET メソッドと全く同じヘスポンスヘッダを要求しますが、レスポンスボディは含みません。これは、リソースのメタデータ(例えば、Content-TypeContent-LengthLast-Modified など)を取得したいが、実際のコンテンツをダウンロードする必要がない場合に非常に役立ちます。例えば、ファイルの存在確認、ファイルのサイズ確認、最終更新日時の確認などに使用されます。

Content-Length ヘッダ

Content-Length ヘッダは、HTTPメッセージボディのオクテット長(バイト数)を示すエンティティヘッダです。これは、クライアントがレスポンスボディの終わりを判断するために使用されます。HEAD リクエストの場合、ボディは存在しませんが、もし GET リクエストであった場合に返されるであろうボディの長さを Content-Length ヘッダで示す必要があります。

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

net/http パッケージは、Go言語におけるHTTPクライアントとサーバーの実装を提供します。このパッケージは、HTTPプロトコルの低レベルな詳細を抽象化し、開発者が簡単にWebアプリケーションやクライアントを構築できるようにします。

  • http.Response 構造体: HTTPレスポンスを表す構造体です。この中に ContentLength フィールドが含まれており、レスポンスボディの長さを格納します。
  • http.Request 構造体: HTTPリクエストを表す構造体です。Method フィールドには、GET, POST, HEAD などのHTTPメソッドが格納されます。
  • http.Transport: HTTPクライアントがリクエストを送信し、レスポンスを受信する際の低レベルな詳細を処理します。コネクションの再利用、プロキシ、TLS設定などを管理します。
  • http.Server: HTTPリクエストをリッスンし、ハンドラにディスパッチするHTTPサーバーを実装します。

技術的詳細

このコミットの主要な変更点は、net/http パッケージ内で HEAD リクエストの Content-Length の処理方法を調整することです。

  1. transfer.go における ContentLength の設定ロジックの変更: readTransfer 関数は、HTTPレスポンスの転送エンコーディングや Content-Length ヘッダに基づいて、レスポンスボディの長さを決定する中心的な役割を担っています。 以前は、fixLength 関数が計算した realLength をそのまま t.ContentLength に設定していました。 今回の変更では、レスポンスであり、かつリクエストメソッドが HEAD の場合、Content-Length ヘッダから直接 ContentLength をパースして設定するように修正されました。これにより、HEAD リクエストであっても、サーバーが Content-Length ヘッダを送信していれば、それが Response.ContentLength に反映されるようになります。

  2. parseContentLength ヘルパー関数の導入: Content-Length ヘッダの値をパースするための新しいヘルパー関数 parseContentLength が導入されました。この関数は、文字列から int64 への変換と、負の値やパースエラーのハンドリングを一元的に行います。これにより、Content-Length のパースロジックが整理され、再利用性が向上しました。

  3. server.go における Content-Length: 0 の自動設定の調整: HTTPサーバー側で、レスポンスボディが空で Content-Length ヘッダが設定されていない場合に、自動的に Content-Length: 0 を設定するロジックがありました。このロジックに w.req.Method != "HEAD" という条件が追加されました。これは、HEAD リクエストの場合、ボディは存在しないが、Content-LengthGET リクエストの場合のボディ長を示すべきであり、必ずしも 0 であるとは限らないためです。この変更により、サーバーが HEAD レスポンスに対して誤って Content-Length: 0 を設定するのを防ぎます。

  4. transport.go における hasBody の定義の修正: persistConn.readLoop 内で、レスポンスがボディを持つかどうかを判断する hasBody 変数の定義が修正されました。以前は resp != nil && resp.ContentLength != 0 でしたが、これに rc.req.Method != "HEAD" が追加されました。これは、HEAD リクエストの場合、ContentLength0 でなくてもボディは存在しないため、HEAD リクエストをボディがないものとして正しく扱うための修正です。

  5. テストケースの追加と修正: client_test.goTestClientHeadContentLength という新しいテストケースが追加されました。このテストは、HEAD リクエストに対するレスポンスで ContentLength が正しく設定されていることを検証します。 response_test.gotransport_test.go の既存のテストケースも、HEAD レスポンスにおける ContentLength の期待値に合わせて修正されました。

これらの変更により、net/http パッケージは HEAD リクエストの Content-Length をより正確に処理し、HTTP仕様への準拠を向上させます。

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

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

  • src/pkg/net/http/client_test.go: HEAD リクエストの Content-Length を検証する新しいテストケース TestClientHeadContentLength が追加されました。
  • src/pkg/net/http/response.go: Response 構造体の ContentLength フィールドのコメントが RequestMethod から Request.Method に修正され、より正確な参照になりました。
  • src/pkg/net/http/response_test.go: HEAD リクエストの ContentLength の期待値が 0 から -1 に変更されました。これは、Content-Length が明示的に設定されていない場合のデフォルト値が -1 であることを反映しています。
  • src/pkg/net/http/server.go: サーバー側で Content-Length: 0 を自動設定するロジックに、HEAD リクエストを除外する条件が追加されました。
  • src/pkg/net/http/transfer.go: readTransfer 関数内で HEAD リクエストの ContentLength を処理するロジックが追加・修正され、parseContentLength ヘルパー関数が導入されました。
  • src/pkg/net/http/transport.go: persistConn.readLoop 内の hasBody の定義が修正され、HEAD リクエストがボディを持たないことを明示的に考慮するようになりました。
  • src/pkg/net/http/transport_test.go: HEAD レスポンスの ContentLength の期待値が 0 から 123 に変更されました。これは、テストサーバーが Content-Length: 123 を返すように設定されているためです。

コアとなるコードの解説

src/pkg/net/http/transfer.go の変更点

--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -294,10 +294,19 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 		return err
 	}
 
-	t.ContentLength, err = fixLength(isResponse, t.StatusCode, t.RequestMethod, t.Header, t.TransferEncoding)
+	realLength, err := fixLength(isResponse, t.StatusCode, t.RequestMethod, t.Header, t.TransferEncoding)
 	if err != nil {
 		return err
 	}
+	if isResponse && t.RequestMethod == "HEAD" {
+		if n, err := parseContentLength(t.Header.get("Content-Length")); err != nil {
+			return err
+		} else {
+			t.ContentLength = n
+		}
+	} else {
+		t.ContentLength = realLength
+	}
 
 	// Trailer
 	t.Trailer, err = fixTrailer(t.Header, t.TransferEncoding)
@@ -310,7 +319,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 	// See RFC2616, section 4.4.
 	switch msg.(type) {
 	case *Response:
-		if t.ContentLength == -1 &&
+		if realLength == -1 &&
 			!chunked(t.TransferEncoding) &&
 			bodyAllowedForStatus(t.StatusCode) {
 			// Unbounded body.
@@ -323,11 +332,11 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 	switch {
 	case chunked(t.TransferEncoding):
 		t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
-	case t.ContentLength >= 0:
+	case realLength >= 0:
 		// TODO: limit the Content-Length. This is an easy DoS vector.
-		t.Body = &body{Reader: io.LimitReader(r, t.ContentLength), closing: t.Close}
+		t.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}
 	default:
-		// t.ContentLength < 0, i.e. "Content-Length" not mentioned in header
+		// realLength < 0, i.e. "Content-Length" not mentioned in header
 		if t.Close {
 			// Close semantics (i.e. HTTP/1.0)
 			t.Body = &body{Reader: r, closing: t.Close}
@@ -434,9 +443,9 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
 	// Logic based on Content-Length
 	cl := strings.TrimSpace(header.get("Content-Length"))
 	if cl != "" {
-		n, err := strconv.ParseInt(cl, 10, 64)
-		if err != nil || n < 0 {
-			return -1, &badStringError{"bad Content-Length", cl}
+		n, err := parseContentLength(cl)
+		if err != nil {
+			return -1, err
 		}
 		return n, nil
 	} else {
@@ -641,3 +650,18 @@ func (b *body) Close() error {
 	}
 	return nil
 }
+
+// parseContentLength trims whitespace from s and returns -1 if no value
+// is set, or the value if it's >= 0.
+func parseContentLength(cl string) (int64, error) {
+	cl = strings.TrimSpace(cl)
+	if cl == "" {
+		return -1, nil
+	}
+	n, err := strconv.ParseInt(cl, 10, 64)
+	if err != nil || n < 0 {
+		return 0, &badStringError{"bad Content-Length", cl}
+	}
+	return n, nil
+
+}

この差分は、readTransfer 関数における ContentLength の設定ロジックの核心部分を示しています。

  • realLength, err := fixLength(...) で、まず一般的な Content-Length の計算を行います。
  • その後の if isResponse && t.RequestMethod == "HEAD" ブロックが追加されました。これは、現在の処理対象がHTTPレスポンスであり、かつ元のリクエストメソッドが HEAD であった場合にのみ実行されます。
  • このブロック内で、t.Header.get("Content-Length") を使ってレスポンスヘッダから Content-Length の値を取得し、新しく導入された parseContentLength 関数でパースしています。
  • パースに成功した場合、その値を t.ContentLength に直接設定します。これにより、HEAD レスポンスであっても、サーバーが提供する Content-Length が優先的に使用されるようになります。
  • else ブロックでは、HEAD リクエストでない場合やレスポンスでない場合は、従来の realLengtht.ContentLength に設定します。
  • また、parseContentLength 関数が新しく追加され、Content-Length ヘッダの文字列を int64 に安全に変換する役割を担っています。空文字列の場合は -1 を返し、パースエラーや負の値の場合はエラーを返します。

src/pkg/net/http/server.go の変更点

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -614,7 +614,7 @@ func (w *response) finishRequest() {
 	// HTTP/1.0 clients keep their "keep-alive" connections alive, and for
 	// HTTP/1.1 clients is just as good as the alternative: sending a
 	// chunked response and immediately sending the zero-length EOF chunk.
-	if w.written == 0 && w.header.get("Content-Length") == "" {
+	if w.written == 0 && w.header.get("Content-Length") == "" && w.req.Method != "HEAD" {
 		w.header.Set("Content-Length", "0")
 	}
 	// If this was an HTTP/1.0 request with keep-alive and we sent a

この差分は、HTTPサーバーがレスポンスを送信する際の Content-Length の自動設定ロジックを示しています。

  • 以前は、w.written == 0 (何も書き込まれていない) かつ w.header.get("Content-Length") == "" (Content-Length ヘッダが設定されていない) の場合に、自動的に Content-Length: 0 を設定していました。
  • 今回の変更で、この条件に && w.req.Method != "HEAD" が追加されました。これにより、HEAD リクエストに対するレスポンスでは、たとえボディが空であっても、サーバーが Content-Length: 0 を自動的に設定しなくなります。これは、HEAD レスポンスの Content-Length は、対応する GET レスポンスのボディ長を示すべきであり、それが 0 とは限らないためです。この修正により、サーバーは HEAD リクエストに対してより正確な Content-Length を返すことができるようになります。

関連リンク

参考にした情報源リンク