If-Modified-Since ヘッダーが全ての HTTP‑date 形式を受け入れるようにパーサを修正
If-Modified-Since ヘッダーの解析ロジックが RFC 9110 で定義された三つの HTTP-date 形式すべてに対応するよう変更され、条件付き GET が期待通りに 304 Not Modified を返すようになりました。
背景
If-Modified-Since はクライアントが最後に取得したリソースの更新時刻をサーバに通知し、キャッシュの有効性を判定する重要なリクエストヘッダーです。
RFC 9110 §5.6.7 ではこのヘッダーが IMF‑fixdate, RFC 850, asctime の三形式を受け入れることが MUST と規定されていますが、Rails の従来実装は Time.rfc2822 を使用しており IMF‑fixdate のみを解析できました。その結果、RFC 850 または asctime 形式が送られた場合はパースに失敗し、not_modified? が false になるためキャッシュが無効化され、フルレスポンスが再送されていました。
この不一致は Last-Modified と Date ヘッダーがすでに Time.httpdate で統一的にパースされている点と対照的で、仕様遵守とキャッシュ効率の両面で修正が必要と判断されました。
技術的な変更
ActionDispatch::Http::Cache::Request#if_modified_since が Time.rfc2822 から Time.httpdate へ置き換えられ、コメントで RFC 9110 の要件が明示されています。
@@
def if_modified_since
if since = get_header(HTTP_IF_MODIFIED_SINCE)
- Time.rfc2822(since) rescue nil
+ # `If-Modified-Since` carries an HTTP-date, which per RFC 9110 may be in
+ # any of the three legal formats (IMF-fixdate, RFC 850, or asctime).
+ # `Time.httpdate` accepts all three; it also matches how the response side
+ # parses `Last-Modified`/`Date`. `Time.rfc2822` only accepted IMF-fixdate.
+ Time.httpdate(since) rescue nil
end
end
さらに actionpack/test/dispatch/request_test.rb に RequestIfModifiedSince テストクラスが追加され、IMF‑fixdate、RFC 850、asctime の三形式が正しく解析され not_modified? が真になることを検証しています。
Time.httpdate は IMF‑fixdate、RFC 850、asctime の三形式すべてをパースでき、GMT タイムゾーンを必須とするため RFC 9110 に完全準拠します。既存の IMF‑fixdate での挙動は変更せず、後方互換性が保たれたまま新規形式がサポートされました。
設計判断
この修正は 最小限の侵襲で仕様遵守を実現 する方針に沿い、独自パーサを実装せず標準ライブラリの Time.httpdate を再利用しています。これにより実装コストとメンテナンス負荷が大幅に削減されました。
リクエスト側とレスポンス側で同一メソッド (Time.httpdate) を使用することで、コードベース全体の一貫性が向上し、将来的なバグ修正や拡張時のリスクが低減します。唯一のトレードオフは、Time.httpdate が GMT 以外のオフセット表記を拒否する点ですが、これは HTTP‑date の仕様に合致するため、非準拠クライアントは 304 を取得できないだけに留まります。
まとめ
If-Modified-Since が RFC 9110 が要求する三つの HTTP-date 形式すべてを受け入れるようになり、条件付き GET の正しいキャッシュ判定が保証されます。実装は Time.httpdate への置換というシンプルな変更であり、既存動作への影響はなく、コードベースの一貫性と仕様遵守が同時に達成されました。