Swift Digest

HTTP date format

HTTP Date Format

Proposal
SF-0016
Authors
Cory Benfield, Tobias, Tony Parker
Review Manager
Tina L
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

HTTP では、サーバが返すレスポンスに必ず Date ヘッダを含めることが RFC 9110 で求められており、その値は RFC 9110 § 5.6.7 で規定された固定フォーマット(Sun, 06 Nov 1994 08:49:37 GMT のような UTC の文字列)でなければなりません。クライアント・サーバを問わず、HTTP を扱うコードでは頻繁にこの形式の文字列を生成・解釈する必要があります。

しかし、Foundation にはこの「HTTP date format」をそのまま扱う API がなく、開発者は DateFormatter のロケール・タイムゾーン設定を慎重に行ったうえで使うか、独自のパーサ・シリアライザを用意するかのいずれかを選ばざるを得ませんでした。

特に HTTP のヘッダ処理は性能が重要な箇所で、毎リクエストごとに汎用の DateFormatter を経由することは現実的ではありません。Swift によるクライアントやサーバ実装が増えるなかで、規格に沿った高性能な HTTP date 専用 API が求められていました。

02 どのように解決されるのか

この提案では、FoundationEssentials に HTTP date format 専用の format style として Date.HTTPFormatStyleDateComponents.HTTPFormatStyle の 2 つが追加されます。いずれも FoundationPreview 6.2 以降で利用でき、API の形は既存の ISO8601FormatStyle に揃えられています。

Date 用の HTTPFormatStyle

Date を HTTP date 文字列にフォーマットしたり、文字列から Date をパースしたりするときは Date.HTTPFormatStyle を使います。FormatStyle / ParseableFormatStyle / ParseStrategy のいずれにも適合しているので、formatted(_:)Date(_:strategy:) のような既存の入り口からそのまま呼び出せます。

let date = Date(timeIntervalSince1970: 784111777)
let formatted = date.formatted(.http)
// "Sun, 06 Nov 1994 08:49:37 GMT"

let parsed = try Date("Sun, 06 Nov 1994 08:49:37 GMT", strategy: .http)

FormatStyle / ParseableFormatStyle / ParseStrategy のそれぞれに http という静的プロパティが用意されているため、文脈に応じて .http だけで参照できます。

さらに Date.HTTPFormatStyleCustomConsumingRegexComponent にも適合しており、Regex builder のなかで HTTP date を直接マッチさせて Date として取り出せます。

let regex = Regex {
    "Date: "
    Capture(.http) // Date.HTTPFormatStyle として使われる
}

if let match = "Date: Sun, 06 Nov 1994 08:49:37 GMT".firstMatch(of: regex) {
    let date: Date = match.1
}

DateComponents 用の HTTPFormatStyle

文字列に含まれる年・月・日・時・分・秒や曜日の値そのものを取り出したい場合は、DateComponents.HTTPFormatStyle を使います。FormatStyle 上のエントリポイントは .httpComponents で、Date 用の .http と区別されています。これは Regex builder のように戻り値の型が文脈から決まらない場面で曖昧さを避けるための分けです。

let components = try DateComponents(
    "Sun, 06 Nov 1994 08:49:37 GMT",
    strategy: .httpComponents
)
// components.year == 1994、components.day == 6、components.hour == 8 など
// components.timeZone == .gmt(仕様により常に GMT)

// 必要なら Date に変換できる
let date = Calendar(identifier: .gregorian).date(from: components)

この提案にあわせて DateComponents 自体にも、他の formattable な型と揃った形で formatted(_:) と、ParseStrategy を受け取る init(_:strategy:) が追加されます。DateComponentsFormatStyle が用意されるのはこれが初めてで、今後ほかの DateComponents 向け format style が増えても同じエントリポイントから使えるようになります。

パース時のフィールドの扱い

パーサは仕様に沿って次のように振る舞います。

  • 曜日(MonTue など)以外のフィールドはすべて必須です。
  • 曜日が含まれているときは Mon から Sun までの正しいトークンであることだけ検証され、Date の生成には使われません。日付から計算される実際の曜日と文字列上の曜日が食い違っていてもエラーにはなりません。
  • 時・分・秒は仕様で定義された範囲(時 0〜23、分 0〜59 など)に収まっているかが検証されます。Foundation はうるう秒を扱わないため、秒の値が 60 だった場合は 0 として扱われます。
  • 日や年の値は妥当な範囲のチェックのみ行われ、たとえば Mon, 99 Jan 2025 19:03:05 GMT のような「99 日」は、Date としてはカレンダーが補正したうえで素直にパースされます。

文字列上の曜日や日と、生成された Date の実際の曜日や日が一致するかまで検証したい場合は、DateComponents 版でパースしてから CalendarDate を生成し、両者を比べる、というパターンが推奨されます。

let strangeDate = "Mon, 99 Jan 2025 19:03:05 GMT"
let date = try Date(strangeDate, strategy: .http)
let components = try DateComponents(strangeDate, strategy: .httpComponents)

let actualDay = Calendar(identifier: .gregorian).component(.day, from: date)
// actualDay != components.day(99 はカレンダー側で繰り上がっている)

曜日の検証を既定動作にしなかったのは、Date を再度コンポーネントに分解するコストが常時かかることを避けるためです。検証が必要な場面では、上記のように DateComponents 側のパーサと組み合わせて呼び出し側で行うことになります。

03 今後の見通し

将来の構想として、HTTP date format 自体は仕様が極めて単純で固定されているため、新たな機能や API 表面の追加が行われる可能性は低い、という見解が示されています。あくまで現時点での見通しであり、将来の追加を約束するものでも、逆に追加を完全に否定するものでもありません。