[インデックス 11540] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールであるgodoc
における、ルートURL (/
) へのリクエスト時に発生していたリダイレクトループのバグを修正するものです。具体的には、URLの正規化処理において、ルートパスに対する末尾のスラッシュの追加ロジックが不適切であったために無限リダイレクトが発生していました。
コミット
commit 702151a2001763aa0b535304377b4b2415141c92
Author: Sameer Ajmani <sameer@golang.org>
Date: Wed Feb 1 09:43:22 2012 -0500
godoc: fix redirect loop for URL "/".
R=golang-dev, bradfitz, rsc, adg
CC=golang-dev
https://golang.org/cl/5606045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/702151a2001763aa0b535304377b4b2415141c92
元コミット内容
godoc: fix redirect loop for URL "/"
.
変更の背景
godoc
は、Go言語のソースコードからドキュメンテーションを生成し、HTTPサーバーとして提供するツールです。ウェブサーバーとして機能する際、URLの正規化(例えば、末尾にスラッシュがないURLにスラッシュを追加してリダイレクトする)は一般的な処理です。しかし、このコミット以前のgodoc
では、ルートURL (/
) に対してこの正規化処理が正しく機能せず、無限リダイレクトが発生していました。
具体的には、ユーザーがブラウザでhttp://localhost:6060
(godoc
のデフォルトポート)にアクセスすると、サーバーはhttp://localhost:6060/
にリダイレクトしようとします。しかし、そのリダイレクト先のURLも内部的に再度正規化の対象となり、結果として同じURLへのリダイレクトが繰り返され、ブラウザが「リダイレクトが多すぎます」といったエラーを表示する状態になっていました。これはユーザーエクスペリエンスを著しく損なうバグであり、修正が必要でした。
前提知識の解説
godoc
godoc
は、Go言語の公式ツールチェーンの一部であり、Goのソースコードからドキュメンテーションを生成し、ウェブブラウザで閲覧できるようにするコマンドラインツールです。Goのパッケージ、関数、型、変数などのドキュメンテーションコメント(doc comments)を解析し、HTML形式で表示します。また、Goのソースコード自体もブラウザから参照できる機能を提供します。
HTTPリダイレクト (HTTP Redirect)
HTTPリダイレクトは、ウェブサーバーがクライアント(ブラウザなど)に対して、要求されたリソースが別のURLに移動したことを伝える仕組みです。これには様々なHTTPステータスコードが使用されますが、このケースではhttp.StatusMovedPermanently
(301) が使われています。301リダイレクトは、リソースが恒久的に移動したことを示し、ブラウザや検索エンジンは新しいURLを記憶します。
URLの正規化 (URL Canonicalization)
URLの正規化とは、同じリソースを指す複数のURL形式を、一つの標準的な形式に統一するプロセスです。ウェブサーバーでは、例えば末尾にスラッシュがあるかないか(例: example.com/path
と example.com/path/
)を統一するためにリダイレクトを用いることがあります。これにより、重複コンテンツの問題を防ぎ、SEO(検索エンジン最適化)にも寄与します。
path.Clean
関数
Go言語のpath
パッケージにあるClean
関数は、パス文字列を「きれいな」形式に変換します。具体的には、冗長なスラッシュ(//
)を一つにまとめたり、.
(カレントディレクトリ)や..
(親ディレクトリ)を解決したりします。例えば、path.Clean("/a/b/../c")
は/a/c
を返します。しかし、Clean
関数は末尾のスラッシュを削除する特性があります。例えば、path.Clean("/a/")
は/a
を返します。この特性が、今回のリダイレクトループの原因の一つとなっていました。
strings.HasSuffix
関数
Go言語のstrings
パッケージにあるHasSuffix
関数は、ある文字列が指定されたサフィックス(接尾辞)で終わるかどうかを判定します。例えば、strings.HasSuffix("filename.txt", ".txt")
はtrue
を返します。
リダイレクトループ (Redirect Loop)
リダイレクトループは、ウェブサーバーがクライアントを無限にリダイレクトし続ける状態を指します。これは通常、URLの正規化ルールやリダイレクト設定が誤っている場合に発生します。例えば、AからBへリダイレクトし、BからAへリダイレクトするような循環参照や、今回のケースのように、正規化処理が常に同じURLへのリダイレクトを指示し続ける場合に発生します。
技術的詳細
このバグは、src/cmd/godoc/godoc.go
ファイル内のredirect
関数に存在していました。この関数は、リクエストされたURLパスを正規化し、必要であれば末尾にスラッシュを追加してリダイレクトを行う役割を担っています。
変更前のコードは以下のようになっていました。
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
if canonical := path.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
redirected = true
}
return
}
このコードの問題点は、path.Clean(r.URL.Path)
がパスをクリーンアップする際に、末尾のスラッシュを削除してしまう点にありました。例えば、リクエストパスが/
の場合、path.Clean("/")
は空文字列""
を返します。これに"/"
を連結すると、canonical
は"/"
となります。
もしリクエストパスが/
であれば、r.URL.Path
は/
であり、canonical
も/
となるため、r.URL.Path != canonical
の条件はfalse
となり、リダイレクトは発生しません。一見問題ないように見えます。
しかし、問題はpath.Clean
の挙動と、godoc
が期待するURLの正規形にありました。godoc
はディレクトリを示すURLには末尾にスラッシュがあることを期待します。
例えば、ユーザーがhttp://localhost:6060
にアクセスした場合、r.URL.Path
は/
です。この場合、path.Clean("/")
は""
を返し、canonical
は"/"
となります。r.URL.Path
(/
) とcanonical
(/
) は等しいため、リダイレクトは発生しません。
では、なぜリダイレクトループが発生したのでしょうか?
これは、godoc
の他の部分で、ルートパスが""
ではなく/
として扱われることを期待している、あるいは、ブラウザがhttp://localhost:6060
をhttp://localhost:6060/
として解釈し、その後の処理でr.URL.Path
が""
になるようなケースがあった可能性があります。
より根本的な問題は、path.Clean
が末尾のスラッシュを削除する特性を考慮せずに、無条件に"/"
を連結していた点です。これにより、例えば/foo/
というパスが/foo
にクリーンアップされ、それに"/"
を付けて/foo/
に戻すという意図しない挙動になっていました。
新しいコードでは、この問題を解決するために、まずpath.Clean
でパスをクリーンアップし、その後に明示的に末尾にスラッシュが必要かどうかをチェックし、必要であれば追加するように変更されました。
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
canonical := path.Clean(r.URL.Path)
if !strings.HasSuffix("/", canonical) { // ここが変更点
canonical += "/"
}
if r.URL.Path != canonical {
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
redirected = true
}
return
}
この修正により、canonical
パスがpath.Clean
によってクリーンアップされた後、それが末尾にスラッシュを持つべきかどうかをstrings.HasSuffix("/", canonical)
で確認します。もしスラッシュがなければ、明示的に追加します。
例えば、リクエストパスが/
の場合:
canonical := path.Clean("/")
->canonical
は""
となる。!strings.HasSuffix("/", "")
はtrue
(空文字列は/
で終わらない)なので、canonical += "/"
が実行され、canonical
は"/"
となる。r.URL.Path
(/
) とcanonical
(/
) は等しいため、リダイレクトは発生しない。
例えば、リクエストパスが/foo
の場合:
canonical := path.Clean("/foo")
->canonical
は/foo
となる。!strings.HasSuffix("/", "/foo")
はtrue
なので、canonical += "/"
が実行され、canonical
は/foo/
となる。r.URL.Path
(/foo
) とcanonical
(/foo/
) は異なるため、/foo/
へリダイレクトされる。
例えば、リクエストパスが/foo/
の場合:
canonical := path.Clean("/foo/")
->canonical
は/foo
となる。!strings.HasSuffix("/", "/foo")
はtrue
なので、canonical += "/"
が実行され、canonical
は/foo/
となる。r.URL.Path
(/foo/
) とcanonical
(/foo/
) は等しいため、リダイレクトは発生しない。
この修正により、path.Clean
の挙動に依存しすぎず、より堅牢に末尾スラッシュの有無を判断し、リダイレクトループを回避できるようになりました。特にルートパス"/"
の扱いが正しくなり、無限リダイレクトが解消されました。
コアとなるコードの変更箇所
src/cmd/godoc/godoc.go
ファイルのredirect
関数が変更されました。
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -499,7 +499,7 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
for _, eg := range examples {
name := eg.Name
- // strip lowercase braz in Foo_braz or Foo_Bar_braz from name
+ // strip lowercase braz in Foo_braz or Foo_Bar_braz from name
// while keeping uppercase Braz in Foo_Braz
if i := strings.LastIndex(name, "_"); i != -1 {
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
@@ -743,7 +743,11 @@ func applyTemplate(t *template.Template, name string, data interface{}) []byte {
}
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
- if canonical := path.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
+ canonical := path.Clean(r.URL.Path)
+ if !strings.HasSuffix("/", canonical) {
+ canonical += "/"
+ }
+ if r.URL.Path != canonical {
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
redirected = true
}
コアとなるコードの解説
変更の核心は、redirect
関数内のURL正規化ロジックです。
変更前:
if canonical := path.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
redirected = true
}
この行では、path.Clean(r.URL.Path)
でパスをクリーンアップした後、無条件に末尾に"/"
を連結していました。path.Clean
は末尾のスラッシュを削除する特性があるため、例えば/
は""
になり、それに"/"
を連結すると/
になります。しかし、この単純な連結では、特定のケース(特にルートパス)で意図しない挙動やリダイレクトループを引き起こす可能性がありました。
変更後:
canonical := path.Clean(r.URL.Path)
if !strings.HasSuffix("/", canonical) {
canonical += "/"
}
if r.URL.Path != canonical {
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
redirected = true
}
この修正では、まずpath.Clean(r.URL.Path)
でパスをクリーンアップし、その結果をcanonical
変数に格納します。
次に、if !strings.HasSuffix("/", canonical)
という条件文が追加されました。これは、「もしcanonical
パスが"/"
で終わっていないならば」という意味です。この条件が真の場合、つまり末尾にスラッシュがない場合にのみ、canonical += "/"
によってスラッシュが追加されます。
この変更により、path.Clean
によってスラッシュが削除されたとしても、その後のstrings.HasSuffix
によるチェックで適切にスラッシュが再追加されるようになります。これにより、ルートパス"/"
が正しく正規化され、無限リダイレクトが解消されました。このアプローチは、より明示的で堅牢なURL正規化を実現しています。
関連リンク
参考にした情報源リンク
- golang/go GitHubリポジトリ
- Go言語の
net/http
パッケージ - URL正規化に関する一般的な情報
- HTTPリダイレクトループに関する一般的な情報
- Goのコードレビューシステム (Gerrit) のCL (Change List) 5606045 (コミットメッセージに記載されているリンク)
- このCLのページは現在アクセスできませんが、当時のGoのコードレビューシステムで使われていた形式です。
- 当時のGoのコードレビューはGerritベースで行われており、
golang.org/cl/
はGerritのCLへのショートリンクでした。 - 現在ではGitHubのPull Requestに移行しています。
- このCLの存在は、この修正が公式なレビュープロセスを経て取り込まれたことを示唆しています。