[インデックス 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))
は、引数 t
を UTC()
に変換してからフォーマットするという関数の実際の動作を正確に反映しています。
テストコード (src/pkg/net/http/serve_test.go
) に TestAppendTime
が追加され、特定のタイムゾーン (CEST
) で初期化された time.Time
オブジェクトが appendTime
を通して正しく GMT
形式に変換され、その後 ParseTime
で元の時刻と一致するかどうかを検証しています。これにより、異なるタイムゾーン環境下でも Date
ヘッダーが正しく機能することが保証されます。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/net/http/export_test.go
:ExportAppendTime = appendTime
という行が追加されました。これは、内部関数であるappendTime
をテスト目的でエクスポートするためのものです。Go言語では、通常、小文字で始まる関数はパッケージ外からアクセスできませんが、_test.go
ファイル内でExport
プレフィックスを付けてエクスポートすることで、テストコードからプライベート関数にアクセスできるようになります。
-
src/pkg/net/http/serve_test.go
:TestAppendTime
という新しいテスト関数が追加されました。- このテストは、
time.Date
を使用してCEST
(中央ヨーロッパ夏時間、UTC+2) のタイムゾーンを持つ特定の時刻t1
を作成します。 ExportAppendTime
を呼び出して、この時刻をバイトスライスにフォーマットします。- フォーマットされた文字列を
ParseTime
で再度パースし、t2
を取得します。 t1.Equal(t2)
を使用して、元の時刻t1
とパースし直した時刻t2
が等しいことを検証します。これにより、タイムゾーン変換が正しく行われ、時刻情報が失われないことを確認します。
- このテストは、
-
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
関数の修正が正しく機能していることを検証するためのものです。
-
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 ではない場合のシナリオをシミュレートするために重要です。
-
res := ExportAppendTime(b[:0], t1)
- テスト目的でエクスポートされた
appendTime
関数 (ExportAppendTime
) を呼び出し、t1
を渡して時刻文字列を生成します。 - この呼び出しの中で、
appendTime
内のt = t.UTC()
が実行され、t1
が UTC に変換されてからフォーマットされます。
- テスト目的でエクスポートされた
-
t2, err := ParseTime(string(res))
net/http
パッケージ内のParseTime
関数(HTTPDate
ヘッダーの解析に使用される)を使用して、appendTime
が生成した時刻文字列res
を再度time.Time
オブジェクトt2
にパースします。
-
if !t1.Equal(t2)
t1.Equal(t2)
は、2つのtime.Time
オブジェクトが同じ瞬間を表しているかどうかを比較します。タイムゾーンが異なっていても、同じ絶対時刻であればtrue
を返します。- このアサーションが成功するということは、
CEST
であったt1
がappendTime
によって正しく UTC に変換され、GMT
形式で出力され、そのGMT
形式の文字列がParseTime
によって元のt1
と同じ絶対時刻として正確にパースされたことを意味します。これにより、Date
ヘッダーの時刻がタイムゾーンに関わらず一貫して扱われることが保証されます。
関連リンク
- Go issue: net/http: Date header sends local time instead of GMT (このコミットが修正した問題のIssue)
- Go CL 13386047: net/http: send correct time in Date header. (このコミットのChange List)
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.18 Date Header): https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
- RFC 1123 - Requirements for Internet Hosts -- Application and Support (Section 5.2.14 Date and Time Formats): https://www.rfc-editor.org/rfc/rfc1123#section-5.2.14
参考にした情報源リンク
- Go
time
package documentation: https://pkg.go.dev/time - Go
net/http
package documentation: https://pkg.go.dev/net/http - UTC vs GMT: https://www.timeanddate.com/time/utc-gmt.html
- Go
_test.go
files and internal functions: https://go.dev/blog/subtests (直接的な言及はないが、テストの慣習について) - Go
time.Time.Equal()
method: https://pkg.go.dev/time#Time.Equal - Go
time.FixedZone()
function: https://pkg.go.dev/time#FixedZone - Go
time.Date()
function: https://pkg.go.dev/time#Date - Go
time.Time.UTC()
method: https://pkg.go.dev/time#Time.UTC - Go
time.Time.Format()
method: https://pkg.go.dev/time#Time.Format - Go
time.Time.Date()
method: https://pkg.go.dev/time#Time.Date - Go
time.Time.Clock()
method: https://pkg.go.dev/time#Time.Clock - Go
time.Time.Weekday()
method: https://pkg.go.dev/time#Time.Weekday - Go
time.Parse()
function: https://pkg.go.dev/time#Parse - Go
time.RFC1123
constant: https://pkg.go.dev/time#RFC1123 - Go
time.TimeFormat
constant (internal tonet/http
): https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/net/http/server.go;l530 (Goソースコード検索) - Go CL 9432046 (potential regression source): https://golang.org/cl/9432046 (このCLは
time
パッケージのFormat
メソッドの最適化に関するもので、直接的な原因ではないが、関連する変更の可能性)