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

[インデックス 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:6060godocのデフォルトポート)にアクセスすると、サーバーは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/pathexample.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:6060http://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)で確認します。もしスラッシュがなければ、明示的に追加します。

例えば、リクエストパスが/の場合:

  1. canonical := path.Clean("/") -> canonical""となる。
  2. !strings.HasSuffix("/", "")true(空文字列は/で終わらない)なので、canonical += "/"が実行され、canonical"/"となる。
  3. r.URL.Path (/) とcanonical (/) は等しいため、リダイレクトは発生しない。

例えば、リクエストパスが/fooの場合:

  1. canonical := path.Clean("/foo") -> canonical/fooとなる。
  2. !strings.HasSuffix("/", "/foo")trueなので、canonical += "/"が実行され、canonical/foo/となる。
  3. r.URL.Path (/foo) とcanonical (/foo/) は異なるため、/foo/へリダイレクトされる。

例えば、リクエストパスが/foo/の場合:

  1. canonical := path.Clean("/foo/") -> canonical/fooとなる。
  2. !strings.HasSuffix("/", "/foo")trueなので、canonical += "/"が実行され、canonical/foo/となる。
  3. 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正規化を実現しています。

関連リンク

参考にした情報源リンク