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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおいて、http.NewRequest 関数がリクエストボディの Content-Length を自動的に設定する際の挙動を改善するものです。具体的には、*bytes.Reader 型のボディが渡された場合にも、そのサイズから Content-Length を正しく推測し、設定するように変更されました。これにより、*strings.Reader*bytes.Buffer と同様に、*bytes.Reader を使用した場合でも、開発者が手動で Content-Length を設定する手間が省かれ、より利便性が向上します。

コミット

commit 5e8ca201d10f7cc5bd555a49aa5202cfc670b4a4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 2 14:40:27 2013 -0800

    net/http: make NewRequest pick a ContentLength from a *bytes.Reader too
    
    It already did so for its sibling, *strings.Reader, as well as *bytes.Buffer.
    
    R=edsrzf, dave, adg, kevlar, remyoudompheng, adg, rsc
    CC=golang-dev
    https://golang.org/cl/7031045

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

https://github.com/golang/go/commit/5e8ca201d10f7cc5bd555a49aa5202cfc670b4a4

元コミット内容

net/http: make NewRequest pick a ContentLength from a *bytes.Reader too

It already did so for its sibling, *strings.Reader, as well as *bytes.Buffer.

R=edsrzf, dave, adg, kevlar, remyoudompheng, adg, rsc
CC=golang-dev
https://golang.org/cl/7031045

変更の背景

http.NewRequest 関数は、HTTPリクエストを作成する際に、リクエストボディの io.Reader から Content-Length ヘッダを自動的に推測し、設定する機能を持っています。これは、特にPOSTリクエストなどでボディを送信する際に、開発者が明示的にボディの長さを計算してヘッダに設定する手間を省くための便利な機能です。

しかし、この機能は *strings.Reader*bytes.Buffer といった特定の io.Reader 実装に対しては既に有効でしたが、*bytes.Reader に対しては適用されていませんでした。*bytes.Reader はバイトスライスをラップして io.Reader インターフェースを提供するものであり、その内部にはデータの長さ情報が含まれています。そのため、*bytes.Reader を使用した場合でも、Content-Length を自動的に設定できるはずでした。

このコミットは、この既存の機能の抜け穴を埋め、*bytes.ReaderContent-Length の自動推測の対象に含めることで、APIの一貫性と利便性を向上させることを目的としています。

前提知識の解説

net/http パッケージ

net/http パッケージは、Go言語におけるHTTPクライアントおよびサーバーの実装を提供する標準ライブラリです。HTTPリクエストの送信、HTTPサーバーの構築、ルーティング、ミドルウェアの適用など、HTTP通信に関する幅広い機能を提供します。

io.Reader インターフェース

io.Reader はGo言語の io パッケージで定義されている基本的なインターフェースの一つです。データを読み出すための単一のメソッド Read(p []byte) (n int, err error) を持ちます。このインターフェースを実装する型は、ファイル、ネットワーク接続、メモリ上のデータなど、様々なデータソースからデータを読み出すことができます。HTTPリクエストのボディは、この io.Reader インターフェースを満たす型として渡されます。

Content-Length ヘッダ

Content-Length はHTTPヘッダの一つで、HTTPメッセージのボディの長さをオクテット(バイト)単位で示します。これは、受信側がボディの終わりを正確に判断し、メッセージ全体を受信したことを確認するために重要です。特にPOSTやPUTリクエストなど、ボディを伴うリクエストでは、このヘッダが正しく設定されていることが通信の成功に不可欠です。

*bytes.Reader

bytes パッケージの Reader 型は、バイトスライス ([]byte) を io.Readerio.Seekerio.ByteReaderio.ByteScannerio.RuneReaderio.RuneScanner インターフェースとしてラップする構造体です。これにより、メモリ上のバイトデータをファイルのように読み出すことができます。Len() メソッドを持ち、残りの読み取り可能なバイト数を返します。

*strings.Reader

strings パッケージの Reader 型は、文字列 (string) を io.Readerio.Seekerio.ByteReaderio.ByteScannerio.RuneReaderio.RuneScanner インターフェースとしてラップする構造体です。*bytes.Reader と同様に、Len() メソッドを持ち、残りの読み取り可能な文字数を返します。

*bytes.Buffer

bytes パッケージの Buffer 型は、可変長のバイトバッファを実装する構造体です。io.Reader および io.Writer インターフェースを実装しており、データの読み書きが可能です。Len() メソッドを持ち、バッファに現在格納されているバイト数を返します。

技術的詳細

http.NewRequest 関数は、リクエストボディとして io.Reader を受け取ります。この関数内部では、渡された io.Reader の具体的な型をチェックし、もしその型が Content-Length を効率的に取得できる特定の型(例えば、Len() メソッドを持つ型)であれば、その長さを使って req.ContentLength フィールドを自動的に設定します。

変更前のコードでは、この型チェックの switch ステートメントにおいて、*strings.Reader*bytes.Buffer は含まれていましたが、*bytes.Reader が含まれていませんでした。

// 変更前
switch v := body.(type) {
case *strings.Reader:
    req.ContentLength = int64(v.Len())
case *bytes.Buffer:
    req.ContentLength = int64(v.Len())
}

*bytes.ReaderLen() メソッドを持っており、そのメソッドは内部のバイトスライスの長さを正確に返すことができます。したがって、*bytes.ReaderContent-Length の自動推測の対象とすることが可能であり、かつ望ましい挙動です。

このコミットでは、switch ステートメントに *bytes.Reader のケースを追加することで、この不足を解消しています。

// 変更後
switch v := body.(type) {
case *bytes.Buffer: // 順序が変更されたが、機能に影響なし
    req.ContentLength = int64(v.Len())
case *bytes.Reader: // 新しく追加されたケース
    req.ContentLength = int64(v.Len())
case *strings.Reader: // 順序が変更されたが、機能に影響なし
    req.ContentLength = int64(v.Len())
}

これにより、http.NewRequest を呼び出す際に bytes.NewReader で作成した io.Reader を渡した場合でも、Content-Length ヘッダが自動的に正しく設定されるようになります。

また、この変更に伴い、request_test.goTestNewRequestContentLength という新しいテスト関数が追加されました。このテストは、*bytes.Reader*bytes.Buffer*strings.Reader のそれぞれについて、NewRequest が正しい ContentLength を設定するかどうかを検証します。さらに、io.Reader インターフェースを満たすが Len() メソッドを持たないカスタム型や、io.NewSectionReader のような Content-Length を推測できないケースについてもテストし、それらの場合には ContentLength が0になることを確認しています。これにより、変更が意図した通りに機能し、かつ他のケースに悪影響を与えないことが保証されます。

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

src/pkg/net/http/request.goNewRequest 関数内の switch ステートメントに、*bytes.Reader のケースが追加されました。

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -433,10 +433,12 @@ func NewRequest(method, urlStr string, body io.Reader) (*Request, error) {
 	}\n \tif body != nil {\n \t\tswitch v := body.(type) {\n-\t\tcase *strings.Reader:\n-\t\t\treq.ContentLength = int64(v.Len())\n \t\tcase *bytes.Buffer:\n \t\t\treq.ContentLength = int64(v.Len())\n+\t\tcase *bytes.Reader:\n+\t\t\treq.ContentLength = int64(v.Len())\n+\t\tcase *strings.Reader:\n+\t\t\treq.ContentLength = int64(v.Len())\n \t\t}\n \t}\n \n```

また、`src/pkg/net/http/request_test.go` に新しいテスト関数 `TestNewRequestContentLength` が追加されました。

```diff
--- a/src/pkg/net/http/request_test.go
+++ b/src/pkg/net/http/request_test.go
@@ -238,6 +238,35 @@ func TestNewRequestHost(t *testing.T) {\n \t}\n }\n \n+func TestNewRequestContentLength(t *testing.T) {\n+\treadByte := func(r io.Reader) io.Reader {\n+\t\tvar b [1]byte\n+\t\tr.Read(b[:])\n+\t\treturn r\n+\t}\n+\ttests := []struct {\n+\t\tr    io.Reader\n+\t\twant int64\n+\t}{\n+\t\t{bytes.NewReader([]byte(\"123\")), 3},\n+\t\t{bytes.NewBuffer([]byte(\"1234\")), 4},\n+\t\t{strings.NewReader(\"12345\"), 5},\n+\t\t// Not detected:\n+\t\t{struct{ io.Reader }{strings.NewReader(\"xyz\")}, 0},\n+\t\t{io.NewSectionReader(strings.NewReader(\"x\"), 0, 6), 0},\n+\t\t{readByte(io.NewSectionReader(strings.NewReader(\"xy\"), 0, 6)), 0},\n+\t}\n+\tfor _, tt := range tests {\n+\t\treq, err := NewRequest(\"POST\", \"http://localhost/\", tt.r)\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tif req.ContentLength != tt.want {\n+\t\t\tt.Errorf(\"ContentLength(%#T) = %d; want %d\", tt.r, req.ContentLength, tt.want)\n+\t\t}\n+\t}\n+}\n+\n func testMissingFile(t *testing.T, req *Request) {\n \tf, fh, err := req.FormFile(\"missing\")\n \tif f != nil {\n```

## コアとなるコードの解説

`src/pkg/net/http/request.go` の変更は、`NewRequest` 関数内の以下の部分にあります。

```go
	if body != nil {
		switch v := body.(type) {
		case *bytes.Buffer:
			req.ContentLength = int64(v.Len())
		case *bytes.Reader: // この行が新しく追加された
			req.ContentLength = int64(v.Len())
		case *strings.Reader:
			req.ContentLength = int64(v.Len())
		}
	}

このコードブロックは、bodynil でない場合に実行されます。bodyio.Reader 型ですが、Goの型アサーション switch v := body.(type) を使用して、body の具体的な動的型をチェックしています。

  • case *bytes.Buffer:: もし body*bytes.Buffer 型であれば、その Len() メソッドを呼び出してバッファの現在の長さを取得し、req.ContentLength に設定します。
  • case *bytes.Reader:: このコミットで追加された部分です。 もし body*bytes.Reader 型であれば、同様にその Len() メソッドを呼び出してリーダーがラップしているバイトスライスの長さを取得し、req.ContentLength に設定します。
  • case *strings.Reader:: もし body*strings.Reader 型であれば、その Len() メソッドを呼び出して文字列の長さを取得し、req.ContentLength に設定します。

この変更により、*bytes.Reader を使用してリクエストボディを構築した場合でも、Content-Length ヘッダが自動的に正しく設定されるようになり、開発者は手動でこのヘッダを設定する手間が省けます。これは、io.Reader インターフェースの背後にある具体的な型が持つ長さ情報を活用することで、よりスマートなAPI挙動を実現しています。

src/pkg/net/http/request_test.go に追加された TestNewRequestContentLength テスト関数は、この変更の正しさを検証します。様々な io.Reader の実装(bytes.NewReader, bytes.NewBuffer, strings.NewReader)に対して NewRequest を呼び出し、req.ContentLength が期待される値と一致するかを確認します。また、Content-Length を推測できないケース(例えば、Len() メソッドを持たないカスタム io.Reader や、部分的に読み取られた io.NewSectionReader)についてもテストし、それらの場合には ContentLength が0になることを確認することで、意図しない副作用がないことを保証しています。

関連リンク

参考にした情報源リンク