署名付きキャッシュペイロードの破損時に安全に nil を返すように修正
ActiveSupport のキャッシュデコーダが、署名付きフレームが不完全な場合に例外を発生させず、エラーレポートしつつ nil を返すようになりました。これにより、破損したペイロードがキャッシュミスとして扱われ、アプリケーション全体への例外伝搬を防ぎます。
背景
ActiveSupport::Cache::Coder#load は署名なしやレガシー形式のペイロードに対してはデシリアライズ失敗時に nil を返す既存のハンドリングを持っていますが、署名付きフレームのヘッダー展開部では保護が欠如していました。具体的には、unpack1 が不足したバイト列に対して ArgumentError(例: "@ outside of string")や TypeError を投げ、例外がそのまま上位に流出していました。結果として、キャッシュミスとして処理されるべきケースがアプリケーションエラーとして顕在化していました。
この問題は、署名は存在するがヘッダーが十分でない "\x00\x11" というペイロードで再現でき、テストが失敗する原因となっていました。
技術的な変更
例外捕捉の追加 により、署名付きヘッダーの解析全体を begin … rescue ArgumentError, TypeError => error でラップしました。捕捉されたエラーは ActiveSupport.error_reporter.report に渡され、ソースが "active_support.cache" として記録されます。その後 return でメソッドを抜け、暗黙的に nil が返ります。
@@
- type = dumped.unpack1(PACKED_TYPE_TEMPLATE)
- expires_at = dumped.unpack1(PACKED_EXPIRES_AT_TEMPLATE)
- version_length = dumped.unpack1(PACKED_VERSION_LENGTH_TEMPLATE)
-
- expires_at = nil if expires_at < 0
- version = load_version(dumped.byteslice(PACKED_VERSION_INDEX, version_length)) if version_length >= 0
- payload = dumped.byteslice((PACKED_VERSION_INDEX + [version_length, 0].max)..)
+ begin
+ type = dumped.unpack1(PACKED_TYPE_TEMPLATE)
+ expires_at = dumped.unpack1(PACKED_EXPIRES_AT_TEMPLATE)
+ version_length = dumped.unpack1(PACKED_VERSION_LENGTH_TEMPLATE)
+
+ expires_at = nil if expires_at < 0
+ version = load_version(dumped.byteslice(PACKED_VERSION_INDEX, version_length)) if version_length >= 0
+ payload = dumped.byteslice((PACKED_VERSION_INDEX + [version_length, 0].max)..)
+ rescue ArgumentError, TypeError => error
+ ActiveSupport.error_reporter.report(error, source: "active_support.cache")
+ return
+ end
テストの拡充 では、署名付きだがヘッダーが不足しているペイロードに対し coder.load が nil を返すこと、かつエラーレポートが行われることを検証するブロックが追加されました。
@@
- lazy_entry = coder.load(payload.byteslice(0..-2))
- assert_raises ActiveSupport::Cache::DeserializationError do
- lazy_entry.value
+ assert_error_reported do
+ assert_nil coder.load(payload.byteslice(0, 2))
+ end
+
+ lazy_entry = coder.load(payload.byteslice(0..-2))
+ assert_raises ActiveSupport::Cache::DeserializationError do
+ lazy_entry.value
設計判断
破損ペイロードの一元化 が主目的です。署名付きパスでも他のケースと同様に例外を捕捉し nil を返すことで、キャッシュ取得ロジック全体が同一の失敗シナリオを想定できるようになりました。新たに導入した ActiveSupport.error_reporter.report は既存のエラーログ機構を流用しており、追加のインフラや設定変更を不要にしています。
後方互換性 も保たれています。署名付きペイロードが正常であれば、begin … rescue ブロックはスルーされ、従来のロジックと同等の性能と振る舞いを維持します。例外が発生しない限り、コードパスは変更前と同一です。
まとめ
今回の修正は、署名付きキャッシュフレームが不完全な場合でも例外が漏れ出さず nil を返すことで、キャッシュミスとして安全に処理できるようにした点が最大の価値です。エラーレポートを残しつつ、一貫した破損ペイロードハンドリングを実現したことで、Rails アプリケーションの堅牢性が向上します。