静的CSSおよびHTMLファイルのContent-TypeにUTF-8チャーセットを追加

rails/rails

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::FileHandlertry_files メソッドに、text/csstext/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/csstext/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/jsonimage/png のような技術的に不要なタイプも含む)していることが言及されています。今回の変更はそれよりも狭く、仕様上根拠のある2タイプのみを対象とするより仕様準拠のアプローチとなっています。

また、Railsスタック全体がUTF-8を前提として統一されており(Encoding.default_externalconfig.encodingActionDispatch::Response.default_charset、ActionViewのテンプレートトランスコーディング)、今回の変更はその一貫性を静的ファイル配信にも波及させるものです。なお、同様の問題を抱えるPropshaftにも対応するコンパニオンPR(rails/propshaft#264)が並行して提出されており、アセット配信経路全体での統一的な対応が図られています。

まとめ

わずか数行の変更ながら、DEFAULT_UTF8_CONTENT_TYPES による対象の明示的な限定と、Railsスタック全体のUTF-8統一という設計方針の延長として位置付けられた整合性の高い修正です。CSSの非ASCII文字に起因するChrome固有の文字化け問題を、仕様上最高優先度のエンコーディングシグナルを活用して確実に解決します。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
eb501bf7

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の3部構成が明確です。リード文、背景、技術的な変更、設計判断、まとめの各要素が適切に配置され、記事の流れが非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト、PR番号のリンク、外部へのリンク記法など、カスタムMarkdown構文がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語を適切に用い、冗長な説明を省くことで、専門知識を持つエンジニアという対象読者に適合した内容になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、高い可読性が確保されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ActionDispatch::Static, MIMEタイプ, charset, RFC 9239など、使用されている技術用語はすべて正確で、文脈に適しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

ChromeのバグやMIMEタイプの選定理由など、技術的な説明はPR情報に基づいており、すべて正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内容で裏付けられており、ハルシネーション(捏造)は一切見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57188)や関連PR番号(rails/propshaft#264)など、記事に含まれる数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトルを的確に要約しており、記事全体の主題もPRの内容と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事内容はすべてPR情報に基づいており、PRで言及されていない外部知識(バージョンのサポート状況など)の追記はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「長年の問題」といった時間表現はPR内の「longstanding bug」と一致しており、時間的な歪曲は見られません。