[インデックス 17839] ファイルの概要
このコミットは、Goプロジェクトのmisc/linkcheck
ツールに対する改善です。具体的には、リンクチェッカーが指定されたルートURLの範囲外へのリダイレクトを追跡しないようにする機能と、問題が発生した場合に非ゼロの終了コードを返すようにする機能が追加されました。これにより、ツールの信頼性と自動化システムでの利用可能性が向上しています。
コミット
commit e7426010c5a577bf2b1e84223036b2c55671f914
Author: Andrew Gerrand <adg@golang.org>
Date: Fri Oct 25 17:31:02 2013 +0300
misc/linkcheck: better redirect handling, use meaningful exit code
Prevent linkcheck from following redirects that lead beyond the outside
the root URL.
Return a non-zero exit code when there are problems.
Some minor refactoring for clarity.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/14425049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e7426010c5a577bf2b1e84223036b2c55671f914
元コミット内容
misc/linkcheck: better redirect handling, use meaningful exit code
このコミットは、linkcheck
ツールがルートURLの外部に繋がるリダイレクトを追跡しないようにし、問題が発生した際に非ゼロの終了コードを返すようにします。また、コードの明確化のための軽微なリファクタリングも含まれています。
変更の背景
misc/linkcheck
ツールは、ウェブサイトやドキュメント内のリンクの有効性をチェックするために使用されます。このツールが直面していた主な課題は以下の通りです。
- 不適切なリダイレクト追跡: 既存の実装では、HTTPリダイレクト(3xxステータスコード)が発生した場合、
linkcheck
はリダイレクト先のURLが元のルートURLの範囲外であっても、無条件にそのリダイレクトを追跡していました。これは、意図しない外部サイトへのリンクチェックを引き起こし、チェックの範囲を不必要に広げたり、関連性のないエラーを報告したりする可能性がありました。例えば、ドキュメントサイトのリンクをチェックしている際に、外部のCDNや認証サービスへのリダイレクトが発生した場合、それらを追跡してしまっていました。 - 不十分なエラー報告: ツールがリンクの問題を検出した場合でも、常にゼロの終了コード(成功を示す)を返していました。これにより、CI/CDパイプラインや自動化スクリプトで
linkcheck
を使用する際に、ツールの実行結果をプログラム的に判断することが困難でした。問題が発生した際に非ゼロの終了コードを返すことは、自動化されたビルドシステムがリンクチェックの失敗を検出し、適切なアクション(例えば、ビルドの停止)を実行するために不可欠です。 - コードの明確性: 既存のコードには、リダイレクト処理やエラー報告のロジックにおいて、改善の余地がありました。
これらの課題に対処するため、このコミットではリダイレクト処理の改善と、より意味のある終了コードの導入が行われました。
前提知識の解説
HTTPリダイレクト (3xx ステータスコード)
HTTPリダイレクトは、ウェブサーバーがクライアント(ブラウザやリンクチェッカーなど)に対して、要求されたリソースが別のURLに移動したことを伝えるメカニズムです。これらは3xxのステータスコードで示されます。
- 301 Moved Permanently: リソースが恒久的に新しいURLに移動したことを示します。
- 302 Found (または Moved Temporarily): リソースが一時的に新しいURLに移動したことを示します。
- 303 See Other: リクエストの結果が別のURLにあることを示し、通常はPOSTリクエストの後にGETリクエストでリソースを取得するために使用されます。
- 307 Temporary Redirect: 302に似ていますが、リクエストメソッドを変更せずにリダイレクトを繰り返すことを保証します。
- 308 Permanent Redirect: 301に似ていますが、リクエストメソッドを変更せずにリダイレクトを繰り返すことを保証します。
Goのnet/http
パッケージのhttp.Get
関数は、デフォルトでこれらのリダイレクトを自動的に追跡します。しかし、より詳細な制御が必要な場合(例えば、特定のリダイレクトを追跡しない場合)は、http.Client
のCheckRedirect
フィールドを設定するか、http.DefaultTransport.RoundTrip
のような低レベルの関数を使用してリダイレクトを手動で処理する必要があります。
Goの net/http
パッケージ
Goの標準ライブラリであるnet/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。
http.Get(url string) (*Response, error)
: 指定されたURLにGETリクエストを送信し、レスポンスを返します。この関数は内部的にリダイレクトを自動的に処理します。http.NewRequest(method, url string, body io.Reader) (*Request, error)
: 指定されたメソッド、URL、ボディを持つ新しいHTTPリクエストを作成します。http.DefaultTransport.RoundTrip(req *Request) (*Response, error)
: HTTPリクエストを送信し、レスポンスを受け取ります。http.Get
とは異なり、この関数はリダイレクトを自動的に追跡しません。これにより、開発者はリダイレクト処理を細かく制御できます。リダイレクトが発生した場合、3xxのステータスコードとLocation
ヘッダー(リダイレクト先のURLを含む)を含むレスポンスが返されます。
プログラムの終了コード
オペレーティングシステムでは、プログラムが終了する際に「終了コード」(または「終了ステータス」)を返します。
- 0: 通常、プログラムが正常に終了したことを示します。
- 非ゼロ: 通常、プログラムの実行中にエラーや問題が発生したことを示します。
この終了コードは、シェルスクリプトやCI/CDツールがプログラムの成功または失敗を判断するために使用されます。例えば、シェルスクリプトではif [ $? -ne 0 ]
のような条件文で直前のコマンドの終了コードをチェックできます。
技術的詳細
このコミットの技術的な変更は、主にmisc/linkcheck/linkcheck.go
ファイルに集中しています。
-
リダイレクト処理の改善:
- 以前は
http.Get(url)
を使用していましたが、これは自動的にリダイレクトを追跡します。 - 新しいコードでは、
http.NewRequest("GET", url, nil)
でリクエストオブジェクトを作成し、http.DefaultTransport.RoundTrip(req)
を使用してリクエストを送信するように変更されました。RoundTrip
はリダイレクトを自動的に追跡しないため、リダイレクトが発生した場合(3xxステータスコードが返された場合)、そのレスポンスを直接受け取ることができます。 res.StatusCode/100 == 3
という条件でリダイレクトを検出し、res.Location()
メソッドでリダイレクト先のURLを取得します。- 取得したリダイレクト先のURLが、
*root
(コマンドライン引数で指定されたルートURL)で始まらない場合、つまりルートURLの範囲外へのリダイレクトである場合は、そのリダイレクトを追跡せずに処理を終了します(return nil
)。これにより、外部サイトへの不必要なリンクチェックが防止されます。 - ルートURLの範囲内へのリダイレクトである場合は、
crawl(newURL.String(), url)
を呼び出して、そのリダイレクト先をチェックキューに追加します。
- 以前は
-
意味のある終了コードの導入:
main
関数の最後に、if len(problems) > 0 { os.Exit(1) }
という行が追加されました。problems
スライスは、リンクチェック中に検出されたすべての問題メッセージを格納します。- この変更により、
linkcheck
ツールが実行を終了する際に、problems
スライスに一つでも問題が記録されていれば、プログラムは非ゼロの終了コード(1
)で終了するようになります。これにより、CI/CDシステムやスクリプトがリンクチェックの失敗を容易に検出できるようになります。
-
リファクタリング:
crawlLoop
関数内のリンクチェックロジックが、新しく導入されたdoCrawl
関数に切り出されました。これにより、crawlLoop
はURLキューからのURLの取得とdoCrawl
の呼び出しに専念し、doCrawl
が個々のURLのクロールとエラー処理を担当するようになり、コードの関心分離が促進され、可読性が向上しました。addProblem
関数内で、*verbose
フラグが設定されている場合にのみlog.Print(msg)
が実行されるようになりました。これにより、冗長モードでない限り、問題メッセージがログに出力されなくなりますが、problems
スライスには引き続き追加されます。
コアとなるコードの変更箇所
misc/linkcheck/linkcheck.go
--- a/misc/linkcheck/linkcheck.go
+++ b/misc/linkcheck/linkcheck.go
@@ -8,11 +8,13 @@
package main
import (
+ "errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
+ "os"
"regexp"
"strings"
"sync"
@@ -101,49 +103,71 @@ func crawl(url string, sourceURL string) {
func addProblem(url, errmsg string) {
msg := fmt.Sprintf("Error on %s: %s (from %s)", url, errmsg, linkSources[url])
- log.Print(msg)
+ if *verbose {
+ log.Print(msg)
+ }
problems = append(problems, msg)
}
func crawlLoop() {
for url := range urlq {
- res, err := http.Get(url)
- if err != nil {
- addProblem(url, fmt.Sprintf("Error fetching: %v", err))
- wg.Done()
- continue
+ if err := doCrawl(url); err != nil {
+ addProblem(url, err.Error())
}
- if res.StatusCode != 200 {
- addProblem(url, fmt.Sprintf("Status code = %d", res.StatusCode))
- wg.Done()
- continue
- }
- slurp, err := ioutil.ReadAll(res.Body)
- res.Body.Close()
- if err != nil {
- log.Fatalf("Error reading %s body: %v", url, err)
+ }
+}
+
+func doCrawl(url string) error {
+ defer wg.Done()
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+ res, err := http.DefaultTransport.RoundTrip(req)
+ if err != nil {
+ return err
+ }
+ // Handle redirects.
+ if res.StatusCode/100 == 3 {
+ newURL, err := res.Location()
+ if err != nil {
+ return fmt.Errorf("resolving redirect: %v", err)
}
- if *verbose {
- log.Printf("Len of %s: %d", url, len(slurp))
+ if !strings.HasPrefix(newURL.String(), *root) {
+ // Skip off-site redirects.
+ return nil
}
- body := string(slurp)
- for _, ref := range localLinks(body) {
- if *verbose {
- log.Printf(" links to %s", ref)
- }
- dest := *root + ref
- linkSources[dest] = append(linkSources[dest], url)
- crawl(dest, url)
+ crawl(newURL.String(), url)
+ return nil
+ }
+ if res.StatusCode != 200 {
+ return errors.New(res.Status)
+ }
+ slurp, err := ioutil.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ log.Fatalf("Error reading %s body: %v", url, err)
+ }
+ if *verbose {
+ log.Printf("Len of %s: %d", url, len(slurp))
+ }
+ body := string(slurp)
+ for _, ref := range localLinks(body) {
+ if *verbose {
+ log.Printf(" links to %s", ref)
}
- for _, id := range pageIDs(body) {
- if *verbose {
- log.Printf(" url %s has #%s", url, id)
- }
- fragExists[urlFrag{url, id}] = true
+ dest := *root + ref
+ linkSources[dest] = append(linkSources[dest], url)
+ crawl(dest, url)
+ }
+ for _, id := range pageIDs(body) {
+ if *verbose {
+ log.Printf(" url %s has #%s", url, id)
}
-
- wg.Done()
+ fragExists[urlFrag{url, id}] = true
}
+ return nil
}
func main() {
@@ -151,7 +175,6 @@ func main() {
go crawlLoop()
crawl(*root, "")
- crawl(*root+"/doc/go1.1.html", "")
wg.Wait()
close(urlq)
@@ -164,4 +187,7 @@ func main() {
for _, s := range problems {
fmt.Println(s)
}
+ if len(problems) > 0 {
+ os.Exit(1)
+ }
}
コアとなるコードの解説
doCrawl
関数の導入とリダイレクト処理
以前のcrawlLoop
関数は、HTTPリクエストの送信、レスポンスの処理、エラーハンドリング、そしてリンクの抽出といった複数の責任を持っていました。このコミットでは、これらの責任の一部をdoCrawl
という新しい関数に切り出すことで、コードのモジュール性と可読性を向上させています。
doCrawl
関数は、単一のURLをクロールし、その過程で発生したエラーをerror
型で返します。
func doCrawl(url string) error {
defer wg.Done() // この関数の終了時にWaitGroupのカウンタを減らす
req, err := http.NewRequest("GET", url, nil) // GETリクエストを作成
if err != nil {
return err // リクエスト作成エラーを返す
}
res, err := http.DefaultTransport.RoundTrip(req) // リクエストを送信(リダイレクトは自動追跡しない)
if err != nil {
return err // HTTP通信エラーを返す
}
// Handle redirects. (リダイレクトの処理)
if res.StatusCode/100 == 3 { // ステータスコードが3xx(リダイレクト)の場合
newURL, err := res.Location() // リダイレクト先のURLを取得
if err != nil {
return fmt.Errorf("resolving redirect: %v", err) // リダイレクト解決エラーを返す
}
if !strings.HasPrefix(newURL.String(), *root) {
// Skip off-site redirects. (ルートURL外へのリダイレクトはスキップ)
return nil
}
crawl(newURL.String(), url) // ルートURL内のリダイレクトは追跡
return nil
}
if res.StatusCode != 200 { // ステータスコードが200 OKでない場合
return errors.New(res.Status) // エラーとしてステータスを返す
}
// 以下、200 OKの場合の通常の処理
slurp, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatalf("Error reading %s body: %v", url, err) // ボディ読み込みエラーは致命的エラーとして扱う
}
// ... (リンクの抽出とクロールキューへの追加) ...
return nil
}
この変更の最も重要な点は、http.Get
からhttp.DefaultTransport.RoundTrip
への移行です。http.Get
はリダイレクトを自動的に処理するため、リダイレクト先のURLがどこであろうと追跡してしまいます。一方、http.DefaultTransport.RoundTrip
はリダイレクトを自動的に追跡しません。これにより、doCrawl
関数内で3xxステータスコードを検出し、res.Location()
でリダイレクト先のURLを取得し、そのURLが*root
で始まるかどうかをstrings.HasPrefix
でチェックすることで、ルートURLの範囲外へのリダイレクトを明示的にスキップできるようになりました。
終了コードの変更
main
関数の最後に以下の行が追加されました。
if len(problems) > 0 {
os.Exit(1) // 問題があれば非ゼロの終了コードで終了
}
これは、linkcheck
ツールが実行中に一つでもリンクの問題(problems
スライスに記録されたエラー)を検出した場合、プログラムがos.Exit(1)
を呼び出して非ゼロの終了コードで終了することを意味します。これにより、シェルスクリプトやCI/CDシステムは、linkcheck
の実行結果を簡単に判断し、リンクチェックが失敗した場合にはビルドを停止するなどの適切なアクションを実行できるようになります。
addProblem
の変更
addProblem
関数は、問題メッセージをproblems
スライスに追加するだけでなく、冗長モード(*verbose
フラグが設定されている場合)でのみログに出力するように変更されました。
func addProblem(url, errmsg string) {
msg := fmt.Sprintf("Error on %s: %s (from %s)", url, errmsg, linkSources[url])
if *verbose { // 冗長モードの場合のみログに出力
log.Print(msg)
}
problems = append(problems, msg) // 問題は常にスライスに追加
}
これは、ツールの出力の制御を改善し、冗長モードでない場合にはコンソールを問題メッセージで埋め尽くさないようにするための軽微なリファクタリングです。
関連リンク
- Go
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - HTTP ステータスコード (MDN Web Docs): https://developer.mozilla.org/ja/docs/Web/HTTP/Status
参考にした情報源リンク
- コミットのGitHubページ: https://github.com/golang/go/commit/e7426010c5a577bf2b1e84223036b2c55671f914
- Go Code Review (Gerrit) の変更リスト: https://golang.org/cl/14425049
- Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Go言語の
strings
パッケージのドキュメント: https://pkg.go.dev/strings