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

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

このコミットは、Go言語の標準ライブラリ net/http/httputil パッケージ内の ReverseProxy において、リバースプロキシがターゲットURLのクエリパラメータと、受信したリクエストのクエリパラメータを適切に結合するように修正するものです。具体的には、既存のクエリパラメータが失われることなく、両方が結合されてバックエンドに転送されるように改善されています。

コミット

  • コミットハッシュ: 518ee115b75c72c68364e1f376d9d9d3f808ffda
  • 作者: Brad Fitzpatrick bradfitz@golang.org
  • コミット日時: 2012年2月7日 18:00:30 -0800
  • コミットメッセージ: net/http/httputil: preserve query params in reverse proxy

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

https://github.com/golang/go/commit/518ee115b75c72c68364e1f376d9d9d3f808ffda

元コミット内容

commit 518ee115b75c72c68364e1f376d9d9d3f808ffda
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Feb 7 18:00:30 2012 -0800

    net/http/httputil: preserve query params in reverse proxy
    
    Fixes #2853
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5642056
---
 src/pkg/net/http/httputil/reverseproxy.go      |  7 ++++-
 src/pkg/net/http/httputil/reverseproxy_test.go | 38 ++++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/src/pkg/net/http/httputil/reverseproxy.go b/src/pkg/net/http/httputil/reverseproxy.go
index 1072e2e342..9c4bd6e09a 100644
--- a/src/pkg/net/http/httputil/reverseproxy.go
+++ b/src/pkg/net/http/httputil/reverseproxy.go
@@ -55,11 +55,16 @@ func singleJoiningSlash(a, b string) string {\n // target's path is "/base" and the incoming request was for "/dir",\n // the target request will be for /base/dir.\n func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {\n+\ttargetQuery := target.RawQuery\n \tdirector := func(req *http.Request) {\n \t\treq.URL.Scheme = target.Scheme\n \t\treq.URL.Host = target.Host\n \t\treq.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)\n-\t\treq.URL.RawQuery = target.RawQuery\n+\t\tif targetQuery == "" || req.URL.RawQuery == "" {\n+\t\t\treq.URL.RawQuery = targetQuery + req.URL.RawQuery\n+\t\t} else {\n+\t\t\treq.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery\n+\t\t}\n \t}\n \treturn &ReverseProxy{Director: director}\n }\ndiff --git a/src/pkg/net/http/httputil/reverseproxy_test.go b/src/pkg/net/http/httputil/reverseproxy_test.go
index 655784b30d..28e9c90ad3 100644
--- a/src/pkg/net/http/httputil/reverseproxy_test.go
+++ b/src/pkg/net/http/httputil/reverseproxy_test.go
@@ -69,3 +69,41 @@ func TestReverseProxy(t *testing.T) {\n \t\tt.Errorf("got body %q; expected %q", g, e)\n \t}\n }\n+\n+var proxyQueryTests = []struct {\n+\tbaseSuffix string // suffix to add to backend URL\n+\treqSuffix  string // suffix to add to frontend's request URL\n+\twant       string // what backend should see for final request URL (without ?)\n+}{\n+\t{"", "", ""},\n+\t{"?sta=tic", "?us=er", "sta=tic&us=er"},\n+\t{"", "?us=er", "us=er"},\n+\t{"?sta=tic", "", "sta=tic"},\n+}\n+\n+func TestReverseProxyQuery(t *testing.T) {\n+\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n+\t\tw.Header().Set("X-Got-Query", r.URL.RawQuery)\n+\t\tw.Write([]byte("hi"))\n+\t}))\n+\tdefer backend.Close()\n+\n+\tfor i, tt := range proxyQueryTests {\n+\t\tbackendURL, err := url.Parse(backend.URL + tt.baseSuffix)\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tfrontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL))\n+\t\treq, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)\n+\t\treq.Close = true\n+\t\tres, err := http.DefaultClient.Do(req)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("%d. Get: %v", i, err)\n+\t\t}\n+\t\tif g, e := res.Header.Get("X-Got-Query"), tt.want; g != e {\n+\t\t\tt.Errorf("%d. got query %q; expected %q", i, g, e)\n+\t\t}\n+\t\tres.Body.Close()\n+\t\tfrontend.Close()\n+\t}\n+}\n```

## 変更の背景

このコミットは、Go言語のIssue #2853「`httputil.ReverseProxy` should preserve query parameters」を修正するために行われました。

`net/http/httputil.ReverseProxy` は、HTTPリクエストを別のネットワークアドレスに転送するリバースプロキシを実装するためのユーティリティです。この機能は、ロードバランシング、APIゲートウェイ、マイクロサービス間のルーティングなど、様々なアーキテクチャで不可欠です。

以前の実装では、`NewSingleHostReverseProxy` 関数が生成する `Director` 関数内で、リクエストのURLの `RawQuery` フィールドをターゲットURLの `RawQuery` で**上書き**していました。これにより、クライアントからリバースプロキシに送られたリクエストが持つクエリパラメータが、バックエンドサーバーに転送される際に失われてしまうという問題がありました。

例えば、リバースプロキシが `http://backend.example.com?param1=value1` をターゲットとして設定されており、クライアントから `http://proxy.example.com/path?param2=value2` というリクエストが来た場合、以前の実装ではバックエンドに転送されるリクエストのURLは `http://backend.example.com/path?param1=value1` となり、`param2=value2` が失われていました。

この挙動は、リバースプロキシの一般的な期待に反します。通常、リバースプロキシはクライアントからのリクエストを可能な限り忠実にバックエンドに転送し、必要に応じて追加の情報を付与したり、既存の情報を変更したりします。クエリパラメータはリクエストの重要な一部であり、バックエンドアプリケーションがリクエストを処理するために利用する情報源であるため、これらが失われることはアプリケーションの機能に直接影響を与えます。

この問題を解決するため、ターゲットURLのクエリパラメータと受信リクエストのクエリパラメータの両方を適切に結合し、バックエンドに転送されるリクエストに含めるように変更されました。

## 前提知識の解説

### 1. HTTPリバースプロキシ

リバースプロキシは、クライアントからのリクエストを受け取り、それを一つまたは複数のバックエンドサーバーに転送するサーバーです。クライアントはリバースプロキシと直接通信しているように見えますが、実際にはリバースプロキシがバックエンドサーバーとの間の仲介役を務めます。

**主な用途:**
*   **ロードバランシング**: 複数のバックエンドサーバーにリクエストを分散し、負荷を均等にする。
*   **セキュリティ**: バックエンドサーバーのIPアドレスを隠蔽し、直接的な攻撃から保護する。
*   **SSL終端**: SSL/TLSハンドシェイクをリバースプロキシで行い、バックエンドサーバーの負荷を軽減する。
*   **キャッシュ**: 静的コンテンツをキャッシュし、バックエンドサーバーへのリクエスト数を減らす。
*   **URLルーティング**: リクエストのURLに基づいて、異なるバックエンドサーバーにルーティングする。

### 2. URLの構造とクエリパラメータ

URL (Uniform Resource Locator) は、インターネット上のリソースを一意に識別するための文字列です。一般的なURLの構造は以下のようになります。

`scheme://host:port/path?query#fragment`

*   **scheme (スキーム)**: リソースにアクセスするためのプロトコル(例: `http`, `https`, `ftp`)。
*   **host (ホスト)**: リソースが配置されているサーバーのドメイン名またはIPアドレス。
*   **port (ポート)**: サーバーがリクエストをリッスンしているポート番号(HTTPのデフォルトは80、HTTPSは443)。
*   **path (パス)**: サーバー上のリソースの場所を示す階層的なパス(例: `/users/profile`)。
*   **query (クエリ)**: リソースに渡される追加のパラメータ。`?` の後に `key=value` 形式のペアが `&` で区切られて続きます(例: `?name=Alice&age=30`)。
*   **fragment (フラグメント)**: リソース内の特定の部分を指す識別子。`#` の後に続きます(通常、クライアント側でのみ使用され、サーバーには送信されません)。

このコミットで特に重要なのは `query` 部分、すなわちクエリパラメータです。

### 3. Go言語の `net/http` および `net/url` パッケージ

*   **`net/http`**: Go言語でHTTPクライアントとサーバーを実装するための主要なパッケージです。HTTPリクエスト、レスポンス、ヘッダー、クッキーなどを扱うための型と関数を提供します。
*   **`net/url`**: URLの解析、構築、エンコード/デコードを行うためのパッケージです。`url.URL` 構造体は、URLの各コンポーネント(Scheme, Host, Path, RawQueryなど)をフィールドとして持ちます。
    *   `url.URL.RawQuery`: URLのクエリ部分(`?` を含まない)を文字列として保持します。例えば、`?param1=value1&param2=value2` の場合、`RawQuery` は `param1=value1&param2=value2` となります。

### 4. `httputil.ReverseProxy` の `Director` 関数

`httputil.ReverseProxy` は、リバースプロキシのロジックをカプセル化する構造体です。その中で最も重要なフィールドの一つが `Director` です。

`Director` は `func(req *http.Request)` 型の関数で、リバースプロキシがバックエンドにリクエストを転送する直前に呼び出されます。この関数は、受信したクライアントリクエスト (`req`) を変更して、バックエンドサーバーに転送されるべき最終的なリクエストの形を決定する役割を担います。例えば、リクエストのURL、ヘッダー、ボディなどを変更することができます。

`NewSingleHostReverseProxy` 関数は、単一のターゲットホストに対するリバースプロキシを簡単に作成するためのヘルパー関数です。この関数は内部で `Director` 関数を生成し、その中で受信リクエストのURLをターゲットURLに基づいて書き換えるロジックを定義しています。

## 技術的詳細

このコミットの核心は、`NewSingleHostReverseProxy` 関数内で定義される `Director` 関数の変更にあります。

以前の `Director` 関数では、以下のように `req.URL.RawQuery` をターゲットURLの `RawQuery` で直接上書きしていました。

```go
req.URL.RawQuery = target.RawQuery

この行は、クライアントから送られてきたリクエストのクエリパラメータを完全に無視し、リバースプロキシが設定されたターゲットURLに元々含まれていたクエリパラメータのみをバックエンドに転送するという挙動を引き起こしていました。これは、リバースプロキシがクライアントからのクエリパラメータをバックエンドに透過的に渡すという一般的な要件を満たしていませんでした。

新しい実装では、この上書きのロジックが改善され、ターゲットURLのクエリパラメータと受信リクエストのクエリパラメータの両方を考慮して結合するようになりました。

変更後のロジックは以下のようになります。

  1. targetQuery := target.RawQuery

    • まず、ターゲットURLの RawQuerytargetQuery という変数に保存します。これは、Director 関数が実行されるたびに target.RawQuery にアクセスするのではなく、一度だけ取得して利用するためです。
  2. if targetQuery == "" || req.URL.RawQuery == "" { ... } else { ... }

    • この条件分岐は、ターゲットURLまたは受信リクエストのいずれか、あるいは両方にクエリパラメータが存在しない場合の最適化と、両方に存在する場合の適切な結合を目的としています。

    • ケース1: ターゲットURLまたは受信リクエストのいずれかにクエリパラメータがない場合 targetQuery == "" || req.URL.RawQuery == "" の条件が真の場合、つまり、ターゲットURLにクエリパラメータがないか、受信リクエストにクエリパラメータがないかのどちらか(または両方)の場合です。

      req.URL.RawQuery = targetQuery + req.URL.RawQuery
      

      この場合、単純に targetQueryreq.URL.RawQuery を連結します。

      • 例1: targetQueryparam1=value1req.URL.RawQuery が空の場合、結果は param1=value1
      • 例2: targetQuery が空で req.URL.RawQueryparam2=value2 の場合、結果は param2=value2
      • 例3: 両方空の場合、結果は空。 この連結は、どちらか一方が空であれば、もう一方のクエリパラメータがそのまま採用されるため、正しい結果が得られます。
    • ケース2: ターゲットURLと受信リクエストの両方にクエリパラメータがある場合 targetQuery != "" かつ req.URL.RawQuery != "" の場合です。

      req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
      

      この場合、targetQueryreq.URL.RawQuery の間に & を挿入して連結します。これは、HTTPのクエリパラメータの標準的な結合方法です。

      • 例: targetQueryparam1=value1req.URL.RawQueryparam2=value2 の場合、結果は param1=value1&param2=value2

この変更により、リバースプロキシはより柔軟になり、ターゲットURLに設定された静的なクエリパラメータと、クライアントから動的に送られてくるクエリパラメータの両方を適切にバックエンドに転送できるようになりました。これは、リバースプロキシの透過性と機能性を向上させる上で重要な改善です。

また、このコミットには、この新しいクエリパラメータ結合ロジックを検証するための新しいテストケース TestReverseProxyQuery が追加されています。これにより、変更が正しく機能すること、および将来のリグレッションを防ぐことが保証されます。テストケースは、様々なクエリパラメータの組み合わせ(両方あり、片方のみあり、両方なし)を網羅しており、堅牢な検証が行われています。

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

src/pkg/net/http/httputil/reverseproxy.goNewSingleHostReverseProxy 関数内の director 関数が変更されています。

--- a/src/pkg/net/http/httputil/reverseproxy.go
+++ b/src/pkg/net/http/httputil/reverseproxy.go
@@ -55,11 +55,16 @@ func singleJoiningSlash(a, b string) string {\n // target's path is " /base" and the incoming request was for " /dir",\n // the target request will be for /base/dir.\n func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {\n+\ttargetQuery := target.RawQuery\n \tdirector := func(req *http.Request) {\n \t\treq.URL.Scheme = target.Scheme\n \t\treq.URL.Host = target.Host\n \t\treq.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)\n-\t\treq.URL.RawQuery = target.RawQuery\n+\t\tif targetQuery == "" || req.URL.RawQuery == "" {\n+\t\t\treq.URL.RawQuery = targetQuery + req.URL.RawQuery\n+\t\t} else {\n+\t\t\treq.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery\n+\t\t}\n \t}\n \treturn &ReverseProxy{Director: director}\n }\

また、src/pkg/net/http/httputil/reverseproxy_test.go に新しいテストケース TestReverseProxyQuery が追加されています。

--- a/src/pkg/net/http/httputil/reverseproxy_test.go
+++ b/src/pkg/net/http/httputil/reverseproxy_test.go
@@ -69,3 +69,41 @@ func TestReverseProxy(t *testing.T) {\n \t\tt.Errorf("got body %q; expected %q", g, e)\n \t}\n }\n+\n+var proxyQueryTests = []struct {\n+\tbaseSuffix string // suffix to add to backend URL\n+\treqSuffix  string // suffix to add to frontend's request URL\n+\twant       string // what backend should see for final request URL (without ?)\n+}{\n+\t{"", "", ""},\n+\t{"?sta=tic", "?us=er", "sta=tic&us=er"},\n+\t{"", "?us=er", "us=er"},\n+\t{"?sta=tic", "", "sta=tic"},\n+}\n+\n+func TestReverseProxyQuery(t *testing.T) {\n+\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n+\t\tw.Header().Set("X-Got-Query", r.URL.RawQuery)\n+\t\tw.Write([]byte("hi"))\n+\t}))\n+\tdefer backend.Close()\n+\n+\tfor i, tt := range proxyQueryTests {\n+\t\tbackendURL, err := url.Parse(backend.URL + tt.baseSuffix)\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tfrontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL))\n+\t\treq, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)\n+\t\treq.Close = true\n+\t\tres, err := http.DefaultClient.Do(req)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("%d. Get: %v", i, err)\n+\t\t}\n+\t\tif g, e := res.Header.Get("X-Got-Query"), tt.want; g != e {\n+\t\t\tt.Errorf("%d. got query %q; expected %q", i, g, e)\n+\t\t}\n+\t\tres.Body.Close()\n+\t\tfrontend.Close()\n+\t}\n+}\n```

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

### `src/pkg/net/http/httputil/reverseproxy.go` の変更

```go
func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
	targetQuery := target.RawQuery // ターゲットURLのRawQueryを事前に取得
	director := func(req *http.Request) {
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
		// ここから変更点
		if targetQuery == "" || req.URL.RawQuery == "" {
			// ターゲットまたはリクエストのどちらか一方(または両方)にクエリがない場合、単純に連結
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			// 両方にクエリがある場合、'&' で結合
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}
	}
	return &ReverseProxy{Director: director}
}
  • targetQuery := target.RawQuery: NewSingleHostReverseProxy が呼び出された時点で、ターゲットURLのクエリ文字列を targetQuery 変数に格納します。これにより、director 関数が呼び出されるたびに target.RawQuery にアクセスするオーバーヘッドを避けます。
  • if targetQuery == "" || req.URL.RawQuery == "": この条件は、ターゲットURLのクエリ文字列 (targetQuery) または受信リクエストのクエリ文字列 (req.URL.RawQuery) のいずれか、または両方が空であるかをチェックします。
    • もし条件が真であれば、req.URL.RawQuery = targetQuery + req.URL.RawQuery となります。これは、どちらか一方が空の場合に、もう一方のクエリ文字列がそのまま最終的なクエリ文字列となるようにします。両方空であれば、結果も空になります。
    • もし条件が偽であれば、つまり targetQueryreq.URL.RawQuery の両方にクエリ文字列が存在する場合です。
      • req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery: この行が、ターゲットURLのクエリと受信リクエストのクエリを & で結合する核心部分です。これにより、両方のクエリパラメータがバックエンドに転送されるリクエストに含まれるようになります。

src/pkg/net/http/httputil/reverseproxy_test.go の追加

var proxyQueryTests = []struct {
	baseSuffix string // suffix to add to backend URL
	reqSuffix  string // suffix to add to frontend's request URL
	want       string // what backend should see for final request URL (without ?)
}{
	{"", "", ""},
	{"?sta=tic", "?us=er", "sta=tic&us=er"},
	{"", "?us=er", "us=er"},
	{"?sta=tic", "", "sta=tic"},
}

func TestReverseProxyQuery(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Got-Query", r.URL.RawQuery) // バックエンドが受け取ったクエリをヘッダーに設定
		w.Write([]byte("hi"))
	}))
	defer backend.Close()

	for i, tt := range proxyQueryTests {
		backendURL, err := url.Parse(backend.URL + tt.baseSuffix) // バックエンドURLにベースサフィックス(ターゲットクエリ)を追加
		if err != nil {
			t.Fatal(err)
		}
		frontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL)) // リバースプロキシをセットアップ
		req, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)     // フロントエンドリクエストURLにリクエストサフィックス(クライアントクエリ)を追加
		req.Close = true
		res, err := http.DefaultClient.Do(req) // リクエストを実行
		if err != nil {
			t.Fatalf("%d. Get: %v", i, err)
		}
		if g, e := res.Header.Get("X-Got-Query"), tt.want; g != e { // バックエンドが受け取ったクエリと期待値を比較
			t.Errorf("%d. got query %q; expected %q", i, g, e)
		}
		res.Body.Close()
		frontend.Close()
	}
}
  • proxyQueryTests 構造体スライス:
    • baseSuffix: バックエンドURL(リバースプロキシのターゲットURL)に付加するクエリ文字列。
    • reqSuffix: フロントエンド(クライアント)からのリクエストURLに付加するクエリ文字列。
    • want: バックエンドサーバーが最終的に受け取るべきクエリ文字列の期待値。
    • このテストデータは、クエリパラメータの様々な組み合わせ(両方あり、片方のみあり、両方なし)を網羅しています。
  • TestReverseProxyQuery 関数:
    • backend := httptest.NewServer(...): テスト用のバックエンドHTTPサーバーを起動します。このサーバーは、受け取ったリクエストの RawQueryX-Got-Query ヘッダーに設定して返します。これにより、リバースプロキシがバックエンドに転送したクエリパラメータを検証できます。
    • ループ内で proxyQueryTests の各テストケースを実行します。
    • backendURL, err := url.Parse(backend.URL + tt.baseSuffix): テストケースの baseSuffix を使って、リバースプロキシのターゲットURLを構築します。
    • frontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL)): 構築したターゲットURLを使って NewSingleHostReverseProxy を呼び出し、リバースプロキシサーバーを起動します。
    • req, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil): テストケースの reqSuffix を使って、クライアントからのリクエストURLを構築します。
    • res, err := http.DefaultClient.Do(req): 構築したリクエストをリバースプロキシに送信します。
    • if g, e := res.Header.Get("X-Got-Query"), tt.want; g != e: バックエンドサーバーが返した X-Got-Query ヘッダーの値(実際に受け取ったクエリ)と、テストケースで定義された期待値 (tt.want) を比較し、一致しない場合はエラーを報告します。

このテストケースの追加により、クエリパラメータの結合ロジックが正しく機能することが保証され、将来の変更によるリグレッションを防ぐための安全網が提供されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: net/http および net/url パッケージ
  • HTTP/1.1 RFC 2616 (特にURLの構造とクエリパラメータに関するセクション)
  • リバースプロキシに関する一般的な情報源 (例: Nginx, Apache HTTP Serverのドキュメント)
  • Go言語のテストに関する情報源 (例: testing パッケージのドキュメント)