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

[インデックス 10045] ファイルの概要

コミット

コミットハッシュ: aa42881ed03c23b89f7eab87768f8669851bc0cc
作成者: Andrew Gerrand adg@golang.org
日付: 2011年10月19日 08:23:13 +1100
コミットメッセージ: http: add test for panic inside hijacked request

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/aa42881ed03c23b89f7eab87768f8669851bc0cc

元コミット内容

このコミットは src/pkg/http/serve_test.go ファイルに新たなテストケース TestHandlerPanicWithHijack を追加し、HTTPハンドラーがハイジャック(Hijack)されたリクエスト内でパニックを発生させた場合の動作を検証するものです。

変更内容:

  • 既存の TestHandlerPanic テストを testHandlerPanic 関数にリファクタリング
  • TestHandlerPanicWithHijack 新規テストケースを追加
  • ハイジャック機能を有効/無効にするフラグ withHijack を導入
  • ハイジャック時の接続管理とクローズ処理を実装

変更の背景

2011年当時、Go言語のHTTPサーバー実装において、ハイジャック機能を使用した際のパニックハンドリングが適切に行われるかどうかを検証するテストが不足していました。HTTPハイジャック機能は、WebSocketなどの別プロトコルに切り替える際に使用される重要な機能であり、このような状況でのパニック処理の安全性を確保することが重要でした。

このテストは、実際のプロダクション環境でハイジャックされたコネクション内でパニックが発生した場合に、サーバーが適切に回復し、リソースリークやデッドロックを防ぐことを保証するために追加されました。

前提知識の解説

HTTPハイジャック(HTTP Hijacking)とは

HTTPハイジャックは、Go言語のnet/httpパッケージが提供する機能で、HTTPハンドラーが元のHTTP接続を「乗っ取る」ことを可能にします。この機能により、開発者は以下のようなことが可能になります:

  1. プロトコルの切り替え: HTTP接続をWebSocket、TCP、または他の独自プロトコルに切り替える
  2. 低レベルな接続制御: net.Connオブジェクトを直接操作して、独自の通信プロトコルを実装
  3. ストリーミング通信: 長時間にわたる双方向通信の実現

Hijackerインターフェイス

type Hijacker interface {
    Hijack() (net.Conn, *bufio.ReadWriter, error)
}

このインターフェイスを実装したResponseWriterは、Hijack()メソッドを呼び出すことで、元のHTTP接続を取得できます。

パニック処理の重要性

Go言語のHTTPサーバーは、ハンドラー内でパニックが発生した場合、通常は以下の動作を行います:

  1. パニックをキャッチし、スタックトレースをログに出力
  2. 適切なHTTPエラーレスポンスをクライアントに送信
  3. 接続をクリーンアップ

しかし、ハイジャックされた接続では、この通常のパニック処理が適用されないため、特別な考慮が必要となります。

技術的詳細

テストの構造

このコミットでは、既存の TestHandlerPanic テストを汎用的な testHandlerPanic 関数に変更し、ハイジャック機能の有無を制御するパラメータを追加しました。

func testHandlerPanic(t *testing.T, withHijack bool)

ハイジャック処理の実装

新しいテストでは、以下のようにハイジャック処理が実装されています:

if withHijack {
    rwc, _, err := w.(Hijacker).Hijack()
    if err != nil {
        t.Logf("unexpected error: %v", err)
    }
    defer rwc.Close()
}

この実装により、以下の処理が行われます:

  1. ResponseWriterをHijackerインターフェイスにキャスト
  2. Hijack()メソッドを呼び出して、net.Conn、bufio.ReadWriter、エラーを取得
  3. エラーハンドリングを実行
  4. deferを使用してコネクションの確実なクローズを保証

ログ出力の制御

テストでは、パニック発生時のログ出力を制御するためにパイプを使用しています:

log.SetOutput(pw)
defer log.SetOutput(os.Stderr)

これにより、テスト実行中のログノイズを抑制し、かつパニック発生時のログが適切に出力されるかを検証できます。

コアとなるコードの変更箇所

1. テスト関数の分離とリファクタリング

変更前:

func TestHandlerPanic(t *testing.T) {
    // テストロジックが直接実装
}

変更後:

func TestHandlerPanic(t *testing.T) {
    testHandlerPanic(t, false)
}

func TestHandlerPanicWithHijack(t *testing.T) {
    testHandlerPanic(t, true)
}

func testHandlerPanic(t *testing.T, withHijack bool) {
    // 共通テストロジック
}

2. ハイジャック機能の追加

キーとなるコード:

ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
    if withHijack {
        rwc, _, err := w.(Hijacker).Hijack()
        if err != nil {
            t.Logf("unexpected error: %v", err)
        }
        defer rwc.Close()
    }
    panic("intentional death for testing")
}))

コアとなるコードの解説

ハイジャック処理の詳細解説

  1. 型アサーション: w.(Hijacker)

    • ResponseWriterがHijackerインターフェイスを実装していることを前提とした型アサーション
    • HTTP/1.xの場合は成功するが、HTTP/2の場合は失敗する可能性がある
  2. 接続の取得: rwc, _, err := w.(Hijacker).Hijack()

    • rwc: 元のnet.Conn接続オブジェクト
    • 2番目の戻り値(bufio.ReadWriter)は使用されていないため、_で破棄
    • err: ハイジャック処理のエラー
  3. エラーハンドリング:

    • ハイジャック処理でエラーが発生した場合、テストログに記録
    • 致命的エラーとして扱わず、テストを継続
  4. リソース管理: defer rwc.Close()

    • パニック発生時でも確実に接続をクローズ
    • メモリリークやファイルディスクリプタリークを防止

パニック発生のタイミング

テストでは、ハイジャック処理の後に意図的にパニックを発生させています:

panic("intentional death for testing")

このタイミングでのパニック発生により、以下のシナリオを検証できます:

  • ハイジャック後のパニック処理が適切に行われるか
  • リソースリークが発生しないか
  • サーバーの安定性が保たれるか

テストの意義

このテストにより、以下の重要な動作が検証されます:

  1. パニック回復: ハイジャック後のパニックが適切にキャッチされる
  2. リソース管理: 接続が確実にクローズされる
  3. サーバー継続性: 他のリクエスト処理に影響しない
  4. ログ出力: パニック発生時のログが適切に記録される

関連リンク

参考にした情報源リンク