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

[インデックス 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ツールは、ウェブサイトやドキュメント内のリンクの有効性をチェックするために使用されます。このツールが直面していた主な課題は以下の通りです。

  1. 不適切なリダイレクト追跡: 既存の実装では、HTTPリダイレクト(3xxステータスコード)が発生した場合、linkcheckはリダイレクト先のURLが元のルートURLの範囲外であっても、無条件にそのリダイレクトを追跡していました。これは、意図しない外部サイトへのリンクチェックを引き起こし、チェックの範囲を不必要に広げたり、関連性のないエラーを報告したりする可能性がありました。例えば、ドキュメントサイトのリンクをチェックしている際に、外部のCDNや認証サービスへのリダイレクトが発生した場合、それらを追跡してしまっていました。
  2. 不十分なエラー報告: ツールがリンクの問題を検出した場合でも、常にゼロの終了コード(成功を示す)を返していました。これにより、CI/CDパイプラインや自動化スクリプトでlinkcheckを使用する際に、ツールの実行結果をプログラム的に判断することが困難でした。問題が発生した際に非ゼロの終了コードを返すことは、自動化されたビルドシステムがリンクチェックの失敗を検出し、適切なアクション(例えば、ビルドの停止)を実行するために不可欠です。
  3. コードの明確性: 既存のコードには、リダイレクト処理やエラー報告のロジックにおいて、改善の余地がありました。

これらの課題に対処するため、このコミットではリダイレクト処理の改善と、より意味のある終了コードの導入が行われました。

前提知識の解説

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.ClientCheckRedirectフィールドを設定するか、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ファイルに集中しています。

  1. リダイレクト処理の改善:

    • 以前は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)を呼び出して、そのリダイレクト先をチェックキューに追加します。
  2. 意味のある終了コードの導入:

    • main関数の最後に、if len(problems) > 0 { os.Exit(1) }という行が追加されました。
    • problemsスライスは、リンクチェック中に検出されたすべての問題メッセージを格納します。
    • この変更により、linkcheckツールが実行を終了する際に、problemsスライスに一つでも問題が記録されていれば、プログラムは非ゼロの終了コード(1)で終了するようになります。これにより、CI/CDシステムやスクリプトがリンクチェックの失敗を容易に検出できるようになります。
  3. リファクタリング:

    • 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) // 問題は常にスライスに追加
}

これは、ツールの出力の制御を改善し、冗長モードでない場合にはコンソールを問題メッセージで埋め尽くさないようにするための軽微なリファクタリングです。

関連リンク

参考にした情報源リンク