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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおいて、Request 構造体に RequestURI フィールドを追加するものです。これにより、HTTPリクエストラインでクライアントから送信されたオリジナルのURI文字列を、URLパッケージによる正規化(canonicalization)を経ずにそのまま保持できるようになります。

コミット

commit 899cd04e214435ee09483231fc3fa03ad270c5e6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Jan 26 14:37:14 2012 -0800

    net/http: add Request.RequestURI field
    
    The new url.URL's parsing can be too canonicalizing for
    certain applications. By keeping the original request URI
    around, we give applications a gross escape hatch while
    keeping the URL package clean and simple for normal uses.
    
    (From a discussion with Gary Burd, Gustavo Niemeyer,
    and Russ Cox.)
    
    Fixes #2782
    
    R=golang-dev, rsc, dsymonds
    CC=golang-dev
    https://golang.org/cl/5580044

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

https://github.com/golang/go/commit/899cd04e214435ee09483231fc3fa03ad270c5e6

元コミット内容

このコミットは、net/http パッケージの Request 構造体に RequestURI という新しいフィールドを追加します。このフィールドは、クライアントからサーバーに送信されたHTTPリクエストライン(RFC 2616, Section 5.1)に含まれる、未変更のRequest-URIを保持することを目的としています。

コミットメッセージによると、url.URL パッケージの新しいパース処理が、特定のアプリケーションにとって「正規化しすぎている(too canonicalizing)」場合があることが問題視されていました。この正規化されたURLでは対応できないケースのために、オリジナルのRequest-URIを保持することで、アプリケーションに「大雑把な脱出ハッチ(gross escape hatch)」を提供しつつ、url パッケージ自体は通常の用途でクリーンかつシンプルに保つという意図があります。

この変更は、Gary Burd、Gustavo Niemeyer、Russ Cox との議論から生まれたものであり、Issue #2782 を修正するものです。

変更の背景

Go言語の net/url パッケージは、URLをパースし、その構成要素(スキーム、ホスト、パス、クエリなど)を構造化された形式で提供します。このパース処理には、URLの正規化(canonicalization)が含まれることが一般的です。正規化とは、URLの異なる表記を標準的な形式に統一するプロセスです。例えば、パスの正規化(/// に変換する、... を解決する)、ポート番号の省略、スキームの小文字化などが含まれます。

しかし、特定のHTTPアプリケーション、特にプロキシサーバー、リバースプロキシ、ルーティングロジックがURIの厳密な文字列マッチングに依存する場合、あるいはレガシーシステムとの互換性を保つ必要がある場合などでは、この自動的な正規化が問題となることがあります。例えば、クライアントが GET /foo/../bar のようなリクエストを送信した場合、正規化されると /bar になるかもしれませんが、アプリケーションは ../ のような非正規化されたパス情報に基づいて特定の処理を行いたい場合があります。

このコミットは、このような正規化によって失われる可能性のあるオリジナルのURI情報を保持するためのメカニズムを提供することで、開発者がより低レベルの、未加工のURIデータにアクセスできるようにすることを目的としています。これにより、url.URL パッケージの設計思想である「クリーンでシンプル」さを維持しつつ、特殊な要件を持つアプリケーションにも対応できるようになります。

前提知識の解説

HTTP Request-URI (RFC 2616, Section 5.1)

HTTP/1.1の仕様であるRFC 2616のセクション5.1「Request-Line」では、HTTPリクエストの最初の行(Request-Line)の形式が定義されています。これは Method SP Request-URI SP HTTP-Version CRLF の形式を取ります。 ここでいう Request-URI は、リクエストの対象となるリソースを識別するURIです。これは、絶対URI(例: http://www.example.com/path/to/resource)、絶対パス(例: /path/to/resource?query=string)、またはアスタリスク(*、OPTIONSメソッドなどで使用)など、いくつかの形式を取り得ます。 このコミットで追加される RequestURI フィールドは、この Request-Line から直接取得される、未加工の Request-URI 文字列を指します。

URLの正規化 (Canonicalization)

URLの正規化とは、同じリソースを指す複数のURL表記を、一貫した単一の標準形式に変換するプロセスです。これには以下のような操作が含まれます。

  • スキームの小文字化: HTTPhttp に。
  • ホスト名の小文字化: EXAMPLE.COMexample.com に。
  • デフォルトポートの削除: http://example.com:80/http://example.com/ に。
  • パスの正規化:
    • %XX エンコーディングのデコード(必要に応じて)。
    • ... の解決: http://example.com/a/../bhttp://example.com/b に。
    • 連続するスラッシュの圧縮: http://example.com//pathhttp://example.com/path に。
  • クエリパラメータの順序付け: ?b=2&a=1?a=1&b=2 に。

net/url パッケージの url.Parse() 関数は、これらの正規化の一部を内部的に行い、パースされたURLを url.URL 構造体として返します。この構造体は、正規化された形式でURLの各コンポーネントを提供します。

net/http パッケージと Request 構造体

net/http パッケージは、Go言語でHTTPクライアントとサーバーを実装するための主要なパッケージです。 http.Request 構造体は、受信したHTTPリクエスト(サーバー側)または送信するHTTPリクエスト(クライアント側)を表します。この構造体には、メソッド、URL、ヘッダー、ボディなどの情報が含まれます。 これまで、リクエストのURI情報は Request.URL フィールド(*url.URL 型)を通じてアクセスされていました。これは net/url パッケージによってパースされ、正規化されたURLオブジェクトです。

技術的詳細

このコミットの主要な変更点は、src/pkg/net/http/request.go にある http.Request 構造体に RequestURI という新しい string 型のフィールドが追加されたことです。

type Request struct {
	// ... 既存のフィールド ...

	// RequestURI is the unmodified Request-URI of the
	// Request-Line (RFC 2616, Section 5.1) as sent by the client
	// to a server. Usually the URL field should be used instead.
	// It is an error to set this field in an HTTP client request.
	RequestURI string

	// ... 既存のフィールド ...
}

このフィールドのコメントには、以下の重要な情報が含まれています。

  • unmodified Request-URI: クライアントから送信された未変更のRequest-URIであること。
  • Usually the URL field should be used instead: 通常は Request.URL フィールドを使用すべきであること。これは、Request.URL が正規化されており、ほとんどのアプリケーションにとってより扱いやすい形式であるためです。
  • It is an error to set this field in an HTTP client request: HTTPクライアントのリクエストではこのフィールドを設定してはならないこと。これは、クライアントがリクエストを送信する際には、Request.URL フィールドからURIが構築されるため、RequestURI を手動で設定すると矛盾が生じる可能性があるためです。

ReadRequest 関数の変更

src/pkg/net/http/request.go 内の ReadRequest 関数は、HTTPリクエストの生データを読み込み、http.Request 構造体にパースする役割を担っています。この関数が変更され、リクエストラインから直接 RequestURI フィールドを読み込むようになりました。

変更前:

	var rawurl string
	req.Method, rawurl, req.Proto = f[0], f[1], f[2]

変更後:

	req.Method, req.RequestURI, req.Proto = f[0], f[1], f[2]
	rawurl := req.RequestURI

この変更により、HTTPリクエストラインの2番目の要素(Request-URI部分)が直接 req.RequestURI に代入されるようになりました。そして、rawurl 変数も req.RequestURI の値で初期化されるため、後続の url.Parse() 呼び出しは引き続き rawurl を使用して req.URL フィールドをパースします。これにより、RequestURI は未加工のまま保持され、URL フィールドは正規化されたURLオブジェクトを保持するという二重の目的が達成されます。

クライアント側の制約

src/pkg/net/http/client.gosend 関数(HTTPクライアントがリクエストを送信する際に呼び出される内部関数)には、Request.RequestURI が設定されている場合にエラーを返すチェックが追加されました。

	if req.RequestURI != "" {
		return nil, errors.New("http: Request.RequestURI can't be set in client requests.")
	}

この制約は、クライアントがリクエストを送信する際には、Request.URL フィールドに基づいてリクエストラインが構築されるため、RequestURI を手動で設定することは意図しない動作や矛盾を引き起こす可能性があるためです。RequestURI は、サーバーが受信したリクエストの「生」のURIを保持するためのものであり、クライアントが送信するリクエストの構築には使用されません。

テストの追加

この変更に伴い、src/pkg/net/http/client_test.gosrc/pkg/net/http/readrequest_test.go にテストが追加・修正されています。

  • TestClientErrorWithRequestURI: クライアント側で RequestURI を設定しようとするとエラーになることを確認するテスト。
  • readrequest_test.goreqTests 変数に、RequestURI フィールドの期待値が追加され、サーバーがリクエストを読み込む際に正しくパースされることを検証しています。

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

src/pkg/net/http/request.go

Request 構造体に RequestURI フィールドが追加されました。 ReadRequest 関数内で、HTTPリクエストラインのURI部分が直接 req.RequestURI に代入されるようになりました。

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -153,6 +153,12 @@ type Request struct {
 	// This field is ignored by the HTTP client.
 	RemoteAddr string
 
+	// RequestURI is the unmodified Request-URI of the
+	// Request-Line (RFC 2616, Section 5.1) as sent by the client
+	// to a server. Usually the URL field should be used instead.
+	// It is an error to set this field in an HTTP client request.
+	RequestURI string
+
 	// TLS allows HTTP servers and other software to record
 	// information about the TLS connection on which the request
 	// was received. This field is not filled in by ReadRequest.
@@ -459,8 +465,8 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
 	if f = strings.SplitN(s, " ", 3); len(f) < 3 {
 		return nil, &badStringError{"malformed HTTP request", s}
 	}
-	var rawurl string
-	req.Method, rawurl, req.Proto = f[0], f[1], f[2]
+	req.Method, req.RequestURI, req.Proto = f[0], f[1], f[2]
+	rawurl := req.RequestURI
 	var ok bool
 	if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok {
 		return nil, &badStringError{"malformed HTTP version", req.Proto}

src/pkg/net/http/client.go

クライアントがリクエストを送信する際に、RequestURI フィールドが設定されている場合にエラーを返すチェックが追加されました。

--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -116,6 +116,10 @@ func send(req *Request, t RoundTripper) (resp *Response, err error) {
 		return nil, errors.New("http: nil Request.URL")
 	}
 
+	if req.RequestURI != "" {
+		return nil, errors.New("http: Request.RequestURI can't be set in client requests.")
+	}
+
 	// Most the callers of send (Get, Post, et al) don't need
 	// Headers, leaving it uninitialized.  We guarantee to the
 	// Transport that this has been initialized, though.

src/pkg/net/http/client_test.go

クライアント側での RequestURI 設定に関するエラーをテストする新しいテストケースが追加されました。

--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -428,3 +428,15 @@ func TestClientInsecureTransport(t *testing.T) {
 		}
 	}
 }
+
+func TestClientErrorWithRequestURI(t *testing.T) {
+	req, _ := NewRequest("GET", "http://localhost:1234/", nil)
+	req.RequestURI = "/this/field/is/illegal/and/should/error/"
+	_, err := DefaultClient.Do(req)
+	if err == nil {
+		t.Fatalf("expected an error")
+	}
+	if !strings.Contains(err.Error(), "RequestURI") {
+		t.Errorf("wanted error mentioning RequestURI; got error: %v", err)
+	}
+}

src/pkg/net/http/readrequest_test.go

reqTests 変数内のテストデータに RequestURI フィールドの期待値が追加されました。

--- a/src/pkg/net/http/readrequest_test.go
+++ b/src/pkg/net/http/readrequest_test.go
@@ -64,6 +64,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 7,\n 			Host:          "www.techcrunch.com",
+			RequestURI:    "http://www.techcrunch.com/",
 		},\n 
 		"abcdef\n",
@@ -89,6 +90,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 0,\n 			Host:          "foo.com",
+			RequestURI:    "/",
 		},\n 
 		noBody,\n@@ -114,6 +116,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 0,\n 			Host:          "test",
+			RequestURI:    "//user@host/is/actually/a/path/",
 		},\n 
 		noBody,\n@@ -163,6 +166,7 @@ var reqTests = []reqTest{\n 			Header:           Header{},\n 			ContentLength:    -1,\n 			Host:             "foo.com",
+			RequestURI:       "/",
 		},\n 
 		"foobar",\n@@ -188,6 +192,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 0,\n 			Host:          "www.google.com:443",
+			RequestURI:    "www.google.com:443",
 		},\n 
 		noBody,\n@@ -211,6 +216,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 0,\n 			Host:          "127.0.0.1:6060",
+			RequestURI:    "127.0.0.1:6060",
 		},\n 
 		noBody,\n@@ -234,6 +240,7 @@ var reqTests = []reqTest{\n 			Close:         false,\n 			ContentLength: 0,\n 			Host:          "",
+			RequestURI:    "/_goRPC_",
 		},\n 
 		noBody,\n```

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

このコミットの核心は、`http.Request` 構造体に `RequestURI` フィールドを追加し、HTTPサーバーが受信したリクエストの「生」のURIをそのまま保持できるようにした点です。

1.  **`RequestURI` フィールドの追加**:
    `Request` 構造体に追加された `RequestURI` は `string` 型であり、HTTPリクエストラインの `Request-URI` 部分を、`net/url` パッケージによるパースや正規化を一切行わずに、そのままの文字列として格納します。これにより、例えば `GET /foo/../bar?q=1` のようなリクエストが来た場合、`RequestURI` は `/foo/../bar?q=1` となり、`URL.Path` は `/bar`、`URL.RawQuery` は `q=1` といった形で、正規化された情報と未加工の情報が両方利用可能になります。

2.  **`ReadRequest` での `RequestURI` の設定**:
    `ReadRequest` 関数は、ネットワークから読み込んだHTTPリクエストのバイト列をパースし、`http.Request` オブジェクトを構築します。この関数内で、リクエストラインをスペースで分割した際に得られる2番目の要素(これがRequest-URIです)が、直接 `req.RequestURI` に代入されるようになりました。その後、この `req.RequestURI` の値が `rawurl` 変数にコピーされ、既存の `url.Parse()` 呼び出しに渡されて `req.URL` フィールドが設定されます。この順序により、`RequestURI` は常にオリジナルの値を含み、`URL` は正規化された値を含むことが保証されます。

3.  **クライアント側での `RequestURI` の使用禁止**:
    `net/http` クライアントがリクエストを送信する際には、`Request.URL` フィールドの情報に基づいてリクエストラインが構築されます。`RequestURI` フィールドは、サーバーが受信したリクエストの生データを保持するためのものであり、クライアントが送信するリクエストの構築には関与しません。そのため、もしクライアント側で `RequestURI` が設定されていた場合、それは意図しない動作や混乱を招く可能性があるため、`send` 関数内で明示的にエラーを返すように変更されました。これは、APIの誤用を防ぐためのガードレールとして機能します。

この変更は、Goの `net/http` パッケージが、一般的な用途では正規化されたURLを推奨しつつも、特定の高度なユースケース(例えば、プロキシ、リバースプロキシ、特定のルーティングロジック、またはレガシーシステムとの互換性など)において、未加工のURIデータへのアクセスが必要となる開発者のニーズに応えるためのものです。これにより、ライブラリの柔軟性が向上し、より幅広いシナリオに対応できるようになりました。

## 関連リンク

*   Go Issue 2782: `net/http: add Request.RequestURI field` - [https://github.com/golang/go/issues/2782](https://github.com/golang/go/issues/2782)
*   Go CL 5580044: `net/http: add Request.RequestURI field` - [https://golang.org/cl/5580044](https://golang.org/cl/5580044)
*   RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 5.1 Request-Line) - [https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1](https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1)

## 参考にした情報源リンク

*   RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
*   Go言語の `net/http` パッケージのドキュメント
*   Go言語の `net/url` パッケージのドキュメント
*   GitHub上のGoリポジトリのコミット履歴と関連Issue
*   URL正規化に関する一般的な情報源 (例: Wikipedia, MDN Web Docs)