[インデックス 14901] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc
コマンドに関連するものです。godoc
は、Goのソースコードからドキュメントを生成し、HTTPサーバーとして提供するツールです。src/cmd/godoc/godoc.go
ファイルは、この godoc
サーバーの主要なロジック、特にHTTPリクエストのハンドリングとリダイレクト処理を実装しています。
コミット
cmd/godoc
におけるリダイレクト処理の修正。リダイレクト時にクエリ文字列が失われないように変更され、http://golang.org/pkg/runtime?m=all
のようなURLが正しく機能するようになりました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c2aee3c0bf7b4199d71970df5557b28b53afae4f
元コミット内容
cmd/godoc: when redirecting don't clear query string
so that http://golang.org/pkg/runtime?m=all works.
R=bradfitz
CC=golang-dev
https://golang.org/cl/7094046
変更の背景
この変更の背景には、godoc
サーバーが特定のURLパターンに対してリダイレクトを行う際に、HTTPリクエストのクエリ文字列(URLの ?
以降の部分)が意図せず失われてしまうという問題がありました。
具体的には、http://golang.org/pkg/runtime?m=all
のようなURLは、runtime
パッケージのドキュメントを表示しつつ、m=all
というクエリパラメータによって、通常は非表示になっているメソッドやフィールドもすべて表示するという機能を提供していました。しかし、godoc
サーバーが内部的にURLの正規化やパスの修正のためにリダイレクトを行う際、既存の実装では http.Redirect
関数に直接新しいパスを渡していました。この http.Redirect
の挙動は、新しいURLが絶対パスで指定された場合、元のリクエストのクエリ文字列を破棄してしまうことがあります。
結果として、ユーザーが ?m=all
のようなクエリパラメータを含むURLにアクセスしても、リダイレクト後にそのパラメータが失われ、期待する表示(例: 全てのメソッド表示)が得られないという不具合が発生していました。このコミットは、この問題を解決し、クエリ文字列がリダイレクト後も保持されるようにするために行われました。
前提知識の解説
godoc
コマンド: Go言語の標準ツールの一つで、Goのソースコードからドキュメントを生成し、Webブラウザで閲覧できるようにHTTPサーバーとして提供します。Goのパッケージや型のドキュメント、コード例などを簡単に参照できるため、Go開発者にとって非常に重要なツールです。- HTTPリダイレクト (HTTP Redirect): Webサーバーがクライアント(ブラウザなど)に対して、要求されたリソースが別のURLに移動したことを伝える仕組みです。HTTPステータスコード(例: 301 Moved Permanently, 302 Found)と
Location
ヘッダーを使って行われます。クライアントはLocation
ヘッダーに示された新しいURLに再度リクエストを送信します。 - URLの構成要素:
- スキーム (Scheme):
http://
やhttps://
の部分。 - ホスト (Host):
golang.org
の部分。 - パス (Path):
/pkg/runtime
の部分。リソースの場所を示します。 - クエリ文字列 (Query String):
?m=all
の部分。キーと値のペア(例:m=all
)で構成され、サーバーに渡す追加のパラメータを指定します。 - フラグメント (Fragment):
#section
のように#
以降の部分。ブラウザ内で特定のセクションにジャンプするために使われ、サーバーには送信されません。
- スキーム (Scheme):
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。http.ResponseWriter
: HTTPレスポンスを書き込むためのインターフェース。*http.Request
: 受信したHTTPリクエストを表す構造体。リクエストメソッド、URL、ヘッダー、ボディなどの情報を含みます。http.Redirect(w http.ResponseWriter, r *http.Request, url string, code int)
: 指定されたURLにクライアントをリダイレクトするためのヘルパー関数。url
引数に新しいリダイレクト先のURLを指定します。*url.URL
構造体:net/url
パッケージで定義されており、URLの各構成要素(スキーム、ホスト、パス、クエリなど)をフィールドとして持つ構造体です。URLの解析や構築に便利です。
技術的詳細
このコミットの技術的な核心は、Goの net/http
パッケージにおける http.Redirect
関数の挙動と、net/url.URL
構造体の利用方法にあります。
元のコードでは、http.Redirect(w, r, canonical, http.StatusMovedPermanently)
のように、リダイレクト先のパス(canonical
)を直接文字列として http.Redirect
関数に渡していました。http.Redirect
関数は内部的に url.Parse
を使用してURLを解析し、Location
ヘッダーを設定します。しかし、url
引数に絶対パス(例: /pkg/runtime/
)のみが与えられた場合、元のリクエスト (r
) のクエリ文字列は新しいリダイレクトURLには引き継がれませんでした。これは、http.Redirect
が url
引数を新しい完全なURLとして解釈し、元のリクエストのURLからパス以外の情報を自動的にマージしないためです。
この問題を解決するために、変更後のコードでは以下の手順を踏んでいます。
r.URL
のコピー:url := *r.URL
という行で、現在のリクエスト (r
) のURL (r.URL
) のコピーを作成しています。r.URL
は*url.URL
型であり、これには元のリクエストのスキーム、ホスト、パス、クエリ文字列など、すべてのURL構成要素が含まれています。ポインタのデリファレンス*r.URL
を使うことで、元のurl.URL
構造体の値が新しい変数url
にコピーされます。これにより、元のリクエストのクエリ文字列がurl
変数に引き継がれます。- パスの更新:
url.Path = canonical
という行で、コピーしたurl
構造体のPath
フィールドのみを、正規化された新しいパス (canonical
) に更新します。この際、url
構造体の他のフィールド(特にRawQuery
フィールドに格納されているクエリ文字列)は変更されません。 - 新しいURL文字列の生成:
url.String()
メソッドを呼び出すことで、更新されたurl
構造体から完全なURL文字列を生成します。このString()
メソッドは、url.URL
構造体のすべての構成要素(スキーム、ホスト、パス、クエリ文字列など)を考慮して、正しい形式のURL文字列を構築します。 - リダイレクトの実行: 最後に、
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
を呼び出し、url.String()
で生成された完全なURLをリダイレクト先として指定します。これにより、クエリ文字列が保持された状態でリダイレクトが行われます。
このアプローチにより、godoc
はURLのパスを正規化しつつも、ユーザーが指定したクエリパラメータを失うことなく、期待通りの動作を維持できるようになりました。
コアとなるコードの変更箇所
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -656,7 +656,9 @@ func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
canonical += "/"
}
if r.URL.Path != canonical {
- http.Redirect(w, r, canonical, http.StatusMovedPermanently)
+ url := *r.URL
+ url.Path = canonical
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
return
@@ -668,7 +670,9 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
c = c[:len(c)-1]
}
if r.URL.Path != c {
- http.Redirect(w, r, c, http.StatusMovedPermanently)
+ url := *r.URL
+ url.Path = c
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
return
コアとなるコードの解説
このコミットでは、src/cmd/godoc/godoc.go
ファイル内の redirect
関数と redirectFile
関数の2箇所が変更されています。両方の関数で同様の修正が適用されています。
redirect
関数の変更点
// 変更前
if r.URL.Path != canonical {
- http.Redirect(w, r, canonical, http.StatusMovedPermanently)
+ url := *r.URL
+ url.Path = canonical
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
url := *r.URL
:r.URL
は現在のHTTPリクエストのURLを表す*url.URL
型のポインタです。*r.URL
はそのポインタが指すurl.URL
構造体の値そのものを取得します。- この行は、現在のリクエストのURL(スキーム、ホスト、パス、クエリ文字列など全てを含む)のコピーを新しい変数
url
に作成しています。これにより、元のリクエストのクエリ文字列がurl
変数に引き継がれます。
url.Path = canonical
:- コピーした
url
構造体のPath
フィールドを、正規化された新しいパスcanonical
に更新します。 - この操作は
url
構造体のPath
フィールドのみを変更し、RawQuery
フィールド(クエリ文字列を保持)を含む他のフィールドには影響を与えません。
- コピーした
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
:url.String()
メソッドは、更新されたurl
構造体から完全なURL文字列を生成します。この文字列には、元のリクエストから引き継がれたクエリ文字列と、新しく設定されたパスが含まれます。- この完全なURL文字列を
http.Redirect
関数に渡すことで、リダイレクト時にクエリ文字列が保持されるようになります。
redirectFile
関数の変更点
redirectFile
関数も同様の目的(ファイルパスのリダイレクト)を持つため、全く同じロジックで修正されています。
// 変更前
if r.URL.Path != c {
- http.Redirect(w, r, c, http.StatusMovedPermanently)
+ url := *r.URL
+ url.Path = c
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
redirected = true
}
ここでも、c
は正規化されたファイルパスを表す文字列であり、redirect
関数における canonical
と同様に扱われます。変更の意図と効果は redirect
関数と同じです。
これらの変更により、godoc
サーバーはURLのパスを修正するリダイレクトを行う際にも、元のリクエストに含まれるクエリ文字列を正しく保持し、ユーザーエクスペリエンスを向上させました。
関連リンク
- Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
net/url
パッケージドキュメント: https://pkg.go.dev/net/url - このコミットのGo Gerrit Code Reviewリンク: https://golang.org/cl/7094046
参考にした情報源リンク
- Go言語の公式ドキュメント
net/http
およびnet/url
パッケージのソースコード- HTTPリダイレクトに関する一般的なWeb技術情報