[インデックス 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-Type
、Content-Length
、Last-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
の処理方法を調整することです。
-
transfer.go
におけるContentLength
の設定ロジックの変更:readTransfer
関数は、HTTPレスポンスの転送エンコーディングやContent-Length
ヘッダに基づいて、レスポンスボディの長さを決定する中心的な役割を担っています。 以前は、fixLength
関数が計算したrealLength
をそのままt.ContentLength
に設定していました。 今回の変更では、レスポンスであり、かつリクエストメソッドがHEAD
の場合、Content-Length
ヘッダから直接ContentLength
をパースして設定するように修正されました。これにより、HEAD
リクエストであっても、サーバーがContent-Length
ヘッダを送信していれば、それがResponse.ContentLength
に反映されるようになります。 -
parseContentLength
ヘルパー関数の導入:Content-Length
ヘッダの値をパースするための新しいヘルパー関数parseContentLength
が導入されました。この関数は、文字列からint64
への変換と、負の値やパースエラーのハンドリングを一元的に行います。これにより、Content-Length
のパースロジックが整理され、再利用性が向上しました。 -
server.go
におけるContent-Length: 0
の自動設定の調整: HTTPサーバー側で、レスポンスボディが空でContent-Length
ヘッダが設定されていない場合に、自動的にContent-Length: 0
を設定するロジックがありました。このロジックにw.req.Method != "HEAD"
という条件が追加されました。これは、HEAD
リクエストの場合、ボディは存在しないが、Content-Length
はGET
リクエストの場合のボディ長を示すべきであり、必ずしも0
であるとは限らないためです。この変更により、サーバーがHEAD
レスポンスに対して誤ってContent-Length: 0
を設定するのを防ぎます。 -
transport.go
におけるhasBody
の定義の修正:persistConn.readLoop
内で、レスポンスがボディを持つかどうかを判断するhasBody
変数の定義が修正されました。以前はresp != nil && resp.ContentLength != 0
でしたが、これにrc.req.Method != "HEAD"
が追加されました。これは、HEAD
リクエストの場合、ContentLength
が0
でなくてもボディは存在しないため、HEAD
リクエストをボディがないものとして正しく扱うための修正です。 -
テストケースの追加と修正:
client_test.go
にTestClientHeadContentLength
という新しいテストケースが追加されました。このテストは、HEAD
リクエストに対するレスポンスでContentLength
が正しく設定されていることを検証します。response_test.go
とtransport_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
リクエストでない場合やレスポンスでない場合は、従来のrealLength
をt.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
を返すことができるようになります。
関連リンク
- Go Change List: https://golang.org/cl/6869053
- Go Issue: #4126 (このコミットで修正された問題)
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 9.4 HEAD): https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
- Go
net/http
package documentation: https://pkg.go.dev/net/http (現在のドキュメント) - Go
net/http
Response
struct: https://pkg.go.dev/net/http#Response (現在のドキュメント)