静的CSSおよびHTMLファイルのContent-TypeにUTF-8チャーセットを追加
ActionDispatch::Static が配信する静的CSSおよびHTMLファイルの Content-Type レスポンスヘッダーに ; charset=utf-8 が付与されるようになりました。これにより、CSSファイル内の非ASCII文字がChrome上で文字化けする長年の問題が解消されます。
背景
Chromeには、CSSファイルのエンコーディング検出に関する既知のバグがあり、<link> タグ経由でスタイルシートを読み込んだ際にUTF-8へのデフォルトフォールバックが機能しないことがあります。Link: rel=preload ヘッダー経由の場合とは異なり、HTML解析中に発見されたスタイルシートではドキュメントのエンコーディングを継承するか、Windows-1252にフォールバックする場合があります。
CSSの仕様上はUTF-8がデフォルトと定められていますが、このChromのバグにより content プロパティに含まれるen-dash(U+2013)などの非ASCII文字が文字化けして表示されます。同様の問題は <meta charset> タグを持たない public/ 配下の静的HTMLファイルにも発生します。
エンコーディング検出の仕様では、Content-Type ヘッダーの charset パラメータが最高優先度のシグナルとして定義されています。このため、ヘッダーに charset=utf-8 を明示することが最も確実な解決策となります。
技術的な変更
ActionDispatch::FileHandler の try_files メソッドに、text/css と text/html のMIMEタイプに対してチャーセットを付与する処理が追加されました。
actionpack/lib/action_dispatch/middleware/static.rb に定数 DEFAULT_UTF8_CONTENT_TYPES が新設され、チャーセットを付与する対象MIMEタイプが明示的に管理されます。
DEFAULT_UTF8_CONTENT_TYPES = [ Mime[:html], Mime[:css] ]
private_constant :DEFAULT_UTF8_CONTENT_TYPES
def try_files(filepath, content_type, accept_encoding:)
headers = { Rack::CONTENT_TYPE => content_type }
if DEFAULT_UTF8_CONTENT_TYPES.include?(content_type)
headers[Rack::CONTENT_TYPE] = "#{content_type}; charset=utf-8"
end
...
end
対象を text/css と text/html の2つに限定していることは、PR本文に明示されているスコープ判断によるものです。text/javascript はRFC 9239で既にcharsetパラメータの扱いが定められているため除外され、text/xml はエンコーディング宣言を持つ場合があるため除外され、text/plain はブラウザバグが確認されていないため対象外となっています。
テスト側では、既存のgzip配信テスト(test_served_gzipped_static_file_with_non_english_filename)が text/html から text/html; charset=utf-8 の期待値に更新されたほか、CSSファイルのチャーセット確認を行う test_serves_static_css_with_charset が新たに追加されています。
設計判断
対象MIMEタイプを定数 DEFAULT_UTF8_CONTENT_TYPES として抽出する設計 が採用されました。条件をコード中に直接書くのではなく名前付き定数に切り出すことで、「なぜこの2つだけか」という意図がコードから読み取れるようになっています。
PR本文では、ActionDispatch::Response が既にすべてのコントローラーレスポンスに対して charset=utf-8 を無条件で付与(application/json や image/png のような技術的に不要なタイプも含む)していることが言及されています。今回の変更はそれよりも狭く、仕様上根拠のある2タイプのみを対象とするより仕様準拠のアプローチとなっています。
また、Railsスタック全体がUTF-8を前提として統一されており(Encoding.default_external、config.encoding、ActionDispatch::Response.default_charset、ActionViewのテンプレートトランスコーディング)、今回の変更はその一貫性を静的ファイル配信にも波及させるものです。なお、同様の問題を抱えるPropshaftにも対応するコンパニオンPR(rails/propshaft#264)が並行して提出されており、アセット配信経路全体での統一的な対応が図られています。
まとめ
わずか数行の変更ながら、DEFAULT_UTF8_CONTENT_TYPES による対象の明示的な限定と、Railsスタック全体のUTF-8統一という設計方針の延長として位置付けられた整合性の高い修正です。CSSの非ASCII文字に起因するChrome固有の文字化け問題を、仕様上最高優先度のエンコーディングシグナルを活用して確実に解決します。