MirrorService#mirror が Blob メタデータを転送するように修正
MirrorService#mirror がコピー先のストレージへ content_type・filename・disposition・custom_metadata を渡さず、application/octet-stream で保存されていた問題を解消します。Blob のメタデータを取得して転送することで、ミラー先でもオリジナルと同一のヘッダーが保持されます。
背景
ActiveStorage の MirrorService#mirror は非同期ジョブ MirrorJob から呼び出され、ミラー先サービスへの upload に checksum: だけを渡していました。その結果、S3・Azure・GCS などヘッダーを保持できるストレージでは content_type が未設定となり、デフォルトの application/octet-stream が付与されました。これにより、CDN のカスタムドメイン経由や <img> タグで直接バケットを参照した場合に、ブラウザがコンテンツを正しく解釈できませんでした。
この不整合は Issue #57270 で報告され、Rails 7.1.5.1 の本番環境で Cloudflare R2 + カスタムドメイン CDN を利用しているケースで確認されています。メタデータが失われると、画像や動画のインライン表示だけでなく、範囲リクエストやキャッシュヒット率にも影響が出るため、根本的な修正が求められました。
技術的な変更
ActiveStorage::Blob#service_metadata がプライベートから公開メソッドへ変更されました。# :nodoc: が付与され、外部から直接呼び出すことは想定されませんが、同一モジュール内の他クラスから安全に利用できます。メソッドは forcibly_serve_as_binary? と allowed_inline? の条件に応じて、返すハッシュに disposition と filename を含めるかを分岐させます。
@@
def service_metadata # :nodoc:
if forcibly_serve_as_binary?
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
elsif !allowed_inline?
{ content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
else
{ content_type: content_type, custom_metadata: custom_metadata }
end
end
同時に、同メソッドの古いプライベート実装が削除され、#service_metadata の唯一の定義は上記に集約されました。これによりコードベースの重複が解消され、メタデータ取得ロジックの一元管理が実現します。
@@
- def service_metadata
- if forcibly_serve_as_binary?
- { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
- elsif !allowed_inline?
- { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
- else
- { content_type: content_type, custom_metadata: custom_metadata }
- end
- end
MirrorService#mirror では、対象キーに紐付く Blob を検索し、取得した service_metadata を upload のキーワード引数として展開するよう改修されました。Blob が見つからない場合は空ハッシュ {} を使用し、従来通りメタデータなしでアップロードします。
@@
- mirrors_in_need_of_mirroring = mirrors_needing_mirroring(key)
- if mirrors_in_need_of_mirroring.any?
+ mirrors_in_need_of_mirroring = mirrors_needing_mirroring(key)
+ if mirrors_in_need_of_mirroring.any?
+ metadata = ActiveStorage::Blob.find_by(key: key)&.service_metadata || {}
@@
- service.upload key, StringIO.new(content), checksum: checksum
+ service.upload key, StringIO.new(content), checksum: checksum, **metadata
テストスイートにも mirroring forwards blob metadata to the mirror services という新規テストが追加され、各ミラーサービスの upload へ渡されたオプションに content_type・filename・disposition が正しく含まれることを検証しています。また、Blob 行が存在しないケースでも例外が発生せずにアップロードが継続されることを保証するテストも併せて提供されています。
@@
- test "mirroring forwards blob metadata to the mirror services" do
+ test "mirroring forwards blob metadata to the mirror services" do
@@
- received.each do |opts|
- assert_equal "text/plain", opts[:content_type]
- assert_equal "report.txt", opts[:filename].to_s
- assert_equal :attachment, opts[:disposition]
- end
+ received.each do |opts|
+ assert_equal "text/plain", opts[:content_type]
+ assert_equal "report.txt", opts[:filename].to_s
+ assert_equal :attachment, opts[:disposition]
+ end
設計判断
Blob#service_metadata を公開メソッドとして提供 した点は、既存コードベースで send を使ってプライベートメソッドにアクセスする手法と比較して、可読性と保守性が向上するという判断に基づきます。unfurl・upload_without_unfurling・compose・mirror_later が同様に # :nodoc: 付きで公開されていることから、内部的に共有すべきロジックは明示的に公開しつつ、外部 API としては文書化しない方針が一貫しています。
メタデータ取得が失敗した場合に 空ハッシュ {} にフォールバック する実装は、キーが孤立した状態(例:手動で削除された Blob)でもミラー処理が中断されないようにする安全策です。これにより、既存のミラー機構に対する破壊的変更を回避しつつ、正常系でのヘッダー保持という新機能を提供しています。
まとめ
MirrorService#mirror が Blob の service_metadata を転送するようになったことで、S3・Azure・GCS などのミラー先でもオリジナルと同一のコンテンツヘッダーが保持され、application/octet-stream に起因する表示不具合が解消されます。Blob#service_metadata の公開と安全なフォールバック設計により、既存コードへの影響を最小限に抑えつつ、ミラー機構全体の一貫性が向上しました。