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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける Date ヘッダーの送信に関するバグ修正です。具体的には、Date ヘッダーが GMT (グリニッジ標準時) を示すと宣言しているにもかかわらず、実際にはローカルタイムゾーンの時刻を送信していた問題を修正し、時刻を UTC (協定世界時) に変換するように変更しました。また、関連する appendTime() 関数のコメントの誤りも修正しています。

コミット

commit c2b7fb3902b9069e24a79343bcad464941e54625
Author: Dmitry Chestnykh <dchest@gmail.com>
Date:   Sun Sep 22 19:53:55 2013 -0700

    net/http: send correct time in Date header.
    
    Date header indicated that it contained GMT time,
    however it actually sent local time. Fixed by
    converting time to UTC.
    
    Also fixes incorrect comment in appendTime().
    
    Regression since CL 9432046.
    
    R=golang-dev, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/13386047

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

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

元コミット内容

net/http: send correct time in Date header.

Date ヘッダーは GMT 時刻を含むと示していたが、実際にはローカル時刻を送信していた。これを UTC に時刻を変換することで修正した。

また、appendTime() 内の誤ったコメントも修正した。

CL 9432046 以降の回帰バグ。

変更の背景

HTTP/1.1 の仕様 (RFC 2616) では、Date ヘッダーはメッセージが生成された日付と時刻を GMT (グリニッジ標準時) で示す必要があります。しかし、Go の net/http パッケージの実装において、Date ヘッダーに設定される時刻が、GMT であるとコメントで示されているにもかかわらず、実際にはサーバーが稼働しているシステムのローカルタイムゾーンの時刻が使用されていました。

この不整合は、クライアント側で Date ヘッダーを解析する際に問題を引き起こす可能性がありました。例えば、クライアントが Date ヘッダーの時刻を GMT として解釈すると、サーバーのローカルタイムゾーンによっては数時間のずれが生じ、キャッシュの有効期限やログのタイムスタンプなどに影響を与える可能性がありました。

コミットメッセージに「Regression since CL 9432046」とあることから、この問題は以前の変更 (CL 9432046) によって導入された回帰バグであることが示唆されています。CL 9432046 は、Go の time パッケージにおける Format メソッドのパフォーマンス改善に関連するコミットである可能性があり、その過程で Date ヘッダーの時刻処理に意図しない変更が加えられたと考えられます。

前提知識の解説

HTTP Date ヘッダー

HTTP レスポンスヘッダーの一つである Date ヘッダーは、メッセージが生成された日付と時刻を示します。HTTP/1.1 の仕様 (RFC 2616, Section 14.18) では、この時刻は GMT (グリニッジ標準時) で表現されるべきであると明確に定められています。フォーマットは RFC1123 形式(例: Mon, 02 Jan 2006 15:04:05 GMT)が推奨されています。

UTC (Coordinated Universal Time) と GMT (Greenwich Mean Time)

  • GMT (グリニッジ標準時): イギリスのグリニッジ天文台を通る子午線(本初子午線)を基準とした時刻です。かつては世界の標準時として広く使われていました。
  • UTC (協定世界時): 国際原子時 (TAI) を基に、うるう秒によって調整される国際的な時刻標準です。現代においては、GMT の後継として事実上の世界標準時として広く採用されています。技術的な文脈では、GMT と UTC はしばしば同義として扱われますが、厳密には異なります。HTTP の文脈では、GMT は UTC と同じ意味で使われることが多いです。

Go言語の time パッケージ

Go言語の time パッケージは、時刻の表現、操作、フォーマット変換を提供します。

  • time.Time 型: 特定の時点を表します。
  • t.UTC(): time.Time オブジェクト t を UTC に変換した新しい time.Time オブジェクトを返します。元のオブジェクトのタイムゾーン情報は変更されません。
  • t.Format(layout): time.Time オブジェクト t を指定されたレイアウト文字列に従ってフォーマットし、文字列として返します。レイアウト文字列は、Go の特別な参照時刻 Mon Jan 2 15:04:05 MST 2006 を基準に定義されます。TimeFormat 定数 ("Mon, 02 Jan 2006 15:04:05 GMT") は、このレイアウト文字列の一例です。

回帰バグ (Regression Bug)

ソフトウェア開発において、以前は正しく動作していた機能が、新しい変更の導入によって再び動作しなくなるバグを指します。このコミットでは、CL 9432046 という以前の変更が原因で Date ヘッダーの時刻がローカルタイムゾーンになってしまったことが示されています。

技術的詳細

このコミットの核心は、net/http パッケージが HTTP Date ヘッダーを生成する際に、time.Time オブジェクトを UTC に明示的に変換するようにした点です。

以前の実装では、appendTime 関数内で time.Time オブジェクトが直接フォーマットされていました。Go の time.Time.Format() メソッドは、デフォルトではその time.Time オブジェクトが保持しているロケーション(タイムゾーン)情報に基づいて時刻をフォーマットします。もし time.Time オブジェクトがローカルタイムゾーンで初期化されていた場合、Format() メソッドはローカルタイムゾーンの時刻を文字列として出力してしまいます。

HTTP Date ヘッダーの仕様では GMT (UTC) が求められているため、appendTime 関数内で t.UTC() を呼び出すことで、時刻を確実に UTC に変換してからフォーマットするように修正されました。これにより、サーバーのローカルタイムゾーン設定に関わらず、常に正しい GMT 形式の Date ヘッダーが送信されるようになります。

また、appendTime 関数のコメントも修正されました。以前のコメント // appendTime is a non-allocating version of []byte(time.Now().UTC().Format(TimeFormat)) は、time.Now().UTC() のように UTC() が適用されることを示唆していましたが、実際の関数シグネチャは appendTime(b []byte, t time.Time) であり、引数 t が必ずしも UTC であるとは限りませんでした。修正後のコメント // appendTime is a non-allocating version of []byte(t.UTC().Format(TimeFormat)) は、引数 tUTC() に変換してからフォーマットするという関数の実際の動作を正確に反映しています。

テストコード (src/pkg/net/http/serve_test.go) に TestAppendTime が追加され、特定のタイムゾーン (CEST) で初期化された time.Time オブジェクトが appendTime を通して正しく GMT 形式に変換され、その後 ParseTime で元の時刻と一致するかどうかを検証しています。これにより、異なるタイムゾーン環境下でも Date ヘッダーが正しく機能することが保証されます。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. src/pkg/net/http/export_test.go:

    • ExportAppendTime = appendTime という行が追加されました。これは、内部関数である appendTime をテスト目的でエクスポートするためのものです。Go言語では、通常、小文字で始まる関数はパッケージ外からアクセスできませんが、_test.go ファイル内で Export プレフィックスを付けてエクスポートすることで、テストコードからプライベート関数にアクセスできるようになります。
  2. src/pkg/net/http/serve_test.go:

    • TestAppendTime という新しいテスト関数が追加されました。
      • このテストは、time.Date を使用して CEST (中央ヨーロッパ夏時間、UTC+2) のタイムゾーンを持つ特定の時刻 t1 を作成します。
      • ExportAppendTime を呼び出して、この時刻をバイトスライスにフォーマットします。
      • フォーマットされた文字列を ParseTime で再度パースし、t2 を取得します。
      • t1.Equal(t2) を使用して、元の時刻 t1 とパースし直した時刻 t2 が等しいことを検証します。これにより、タイムゾーン変換が正しく行われ、時刻情報が失われないことを確認します。
  3. src/pkg/net/http/server.go:

    • appendTime 関数の内部に t = t.UTC() という行が追加されました。これがこのコミットの最も重要な変更点です。これにより、appendTime に渡された time.Time オブジェクトが、その後のフォーマット処理の前に必ず UTC に変換されるようになります。
    • appendTime 関数のコメントが // appendTime is a non-allocating version of []byte(time.Now().UTC().Format(TimeFormat)) から // appendTime is a non-allocating version of []byte(t.UTC().Format(TimeFormat)) に修正されました。これは、関数が引数 t を UTC に変換してフォーマットするという実際の動作をより正確に記述しています。

コアとなるコードの解説

src/pkg/net/http/server.go の変更

// appendTime is a non-allocating version of []byte(t.UTC().Format(TimeFormat))
func appendTime(b []byte, t time.Time) []byte {
	const days = "SunMonTueWedThuFriSat"
	const months = "JanFebMarAprMayJunJulAugSepOctNovDec"

	t = t.UTC() // ★ 追加された行
	yy, mm, dd := t.Date()
	hh, mn, ss := t.Clock()
	day := days[3*t.Weekday():]
	// ... (時刻フォーマットのロジックが続く)
}

この変更は非常にシンプルですが、その影響は大きいです。t = t.UTC() が追加されることで、appendTime 関数に渡される time.Time オブジェクト t は、その後の日付や時刻の各要素(年、月、日、時、分、秒など)を抽出する前に、必ず協定世界時 (UTC) に変換されます。

Go の time.Time 型は、時刻の値だけでなく、その時刻がどのタイムゾーン(ロケーション)で解釈されるべきかという情報も内部的に持っています。t.Date()t.Clock() のようなメソッドは、このロケーション情報に基づいて日付や時刻の要素を返します。もし t がローカルタイムゾーンのロケーション情報を持っていた場合、これらのメソッドはローカルタイムゾーンでの日付や時刻を返してしまいます。

t.UTC() を呼び出すことで、t は同じ瞬間を表すものの、ロケーション情報が UTC に設定された新しい time.Time オブジェクトに置き換えられます。これにより、その後の t.Date()t.Clock() の呼び出しは、常に UTC での時刻要素を返すようになり、結果として TimeFormat (GMT を明示) に従ってフォーマットされた文字列が、実際に GMT (UTC) の時刻を示すようになります。

コメントの修正も重要です。以前のコメントは time.Now().UTC() のように、呼び出し元で既に UTC に変換されているかのような誤解を与える可能性がありました。新しいコメント t.UTC().Format(TimeFormat) は、関数内で引数 t が UTC に変換されることを明確に示しており、関数の動作を正確に反映しています。

src/pkg/net/http/serve_test.go の追加テスト

func TestAppendTime(t *testing.T) {
	var b [len(TimeFormat)]byte
	t1 := time.Date(2013, 9, 21, 15, 41, 0, 0, time.FixedZone("CEST", 2*60*60))
	res := ExportAppendTime(b[:0], t1)
	t2, err := ParseTime(string(res))
	if err != nil {
		t.Fatalf("Error parsing time: %s", err)
	}
	if !t1.Equal(t2) {
		t.Fatalf("Times differ; expected: %v, got %v (%s)", t1, t2, string(res))
	}
}

このテストは、appendTime 関数の修正が正しく機能していることを検証するためのものです。

  1. t1 := time.Date(2013, 9, 21, 15, 41, 0, 0, time.FixedZone("CEST", 2*60*60))

    • time.Date を使用して、特定の年月日、時分秒、ナノ秒、そしてタイムゾーンを指定して time.Time オブジェクト t1 を作成しています。
    • time.FixedZone("CEST", 2*60*60) は、タイムゾーンが CEST (中央ヨーロッパ夏時間) であり、UTC から +2 時間のオフセットを持つことを示しています。これは、ローカルタイムゾーンが UTC ではない場合のシナリオをシミュレートするために重要です。
  2. res := ExportAppendTime(b[:0], t1)

    • テスト目的でエクスポートされた appendTime 関数 (ExportAppendTime) を呼び出し、t1 を渡して時刻文字列を生成します。
    • この呼び出しの中で、appendTime 内の t = t.UTC() が実行され、t1 が UTC に変換されてからフォーマットされます。
  3. t2, err := ParseTime(string(res))

    • net/http パッケージ内の ParseTime 関数(HTTP Date ヘッダーの解析に使用される)を使用して、appendTime が生成した時刻文字列 res を再度 time.Time オブジェクト t2 にパースします。
  4. if !t1.Equal(t2)

    • t1.Equal(t2) は、2つの time.Time オブジェクトが同じ瞬間を表しているかどうかを比較します。タイムゾーンが異なっていても、同じ絶対時刻であれば true を返します。
    • このアサーションが成功するということは、CEST であった t1appendTime によって正しく UTC に変換され、GMT 形式で出力され、その GMT 形式の文字列が ParseTime によって元の t1 と同じ絶対時刻として正確にパースされたことを意味します。これにより、Date ヘッダーの時刻がタイムゾーンに関わらず一貫して扱われることが保証されます。

関連リンク

参考にした情報源リンク