CSS・HTMLアセットのContent-Typeにcharset=utf-8を追加

rails/propshaft

Propshaft::Serverが配信するCSSおよびHTMLアセットのContent-Typeヘッダーに; charset=utf-8が付与されるようになりました。これにより、Chromeの文字コード判定バグに起因するCSSの文字化けを防ぎます。

背景

Propshaft::Serverが返すCSSファイルのContent-Typeはtext/cssのみで、文字コード情報を含んでいませんでした。CSSの仕様はデフォルトエンコーディングとしてUTF-8を定めていますが、Chromeにはlongstanding bugがあり、<link>タグ経由でスタイルシートを発見した場合(Link: rel=preloadヘッダー経由ではない場合)、このデフォルトに従わずドキュメントのエンコーディングを引き継いだりWindows-1252にフォールバックすることがあります。

その結果、CSSのcontentプロパティ内のen-dash(U+2013)などの非ASCII文字が文字化けして表示されるという問題が発生します。同様の問題はHTMLアセットにも該当します。エンコーディング検出チェーンにおいて最優先のシグナルはContent-Typeヘッダーのcharsetパラメータであるため、ここにcharset=utf-8を明示することが最も確実な解決策です。

なお、ActionDispatch::Responseはコントローラーレスポンスに対してcharset=utf-8を自動付与しますが、Propshaft::Serverは独自のRackレスポンスを構築するためこのパスを通りません。本PRはその差分を埋めるものです。同様の修正はコンパニオンPR rails/rails#57188 としてActionDispatch::FileHandlerにも適用されています。

技術的な変更

Propshaft::Assetcontent_type_with_charsetメソッドを追加し、Propshaft::Serverがそれを利用してContent-Typeヘッダーを構築するよう変更されました。

lib/propshaft/asset.rbへの追加:

CONTENT_TYPES_WITH_UTF8_CHARSET = [ Mime::Type.lookup("text/html"), Mime::Type.lookup("text/css") ]

def content_type_with_charset
  if content_type.in? CONTENT_TYPES_WITH_UTF8_CHARSET
    "#{content_type}; charset=utf-8"
  else
    content_type
  end
end

併せて、既存のcontent_typeメソッドにはメモ化@content_type ||=)が追加されました。content_type_with_charsetからcontent_typeを複数回呼び出すケースに備えた最適化です。

lib/propshaft/server.rbでの利用:

# 変更前
Rack::CONTENT_TYPE => asset.content_type.to_s,

# 変更後
Rack::CONTENT_TYPE => asset.content_type_with_charset.to_s,

charset付与の対象はtext/csstext/htmlの2種類のみです。PRには対象を絞った理由も明示されています:

  • text/javascript: RFC 9239でcharsetパラメータを持たないと規定されているため除外
  • text/xml: エンコーディング宣言(<?xml version="1.0" encoding="..."?>)と競合する可能性があるため除外
  • text/plain: ブラウザの実証されたバグがないため除外

設計判断

charsetの付与ロジックをPropshaft::Assetクラス内に定数CONTENT_TYPES_WITH_UTF8_CHARSETとして定義し、メソッドとして公開する設計が採用されました。

サーバー側(Propshaft::Server)でContent-Typeをインライン加工する方法も考えられますが、アセット自体がcharsetを認識するべきという責務の分離を優先した判断といえます。Propshaft::Serverの変更は呼び出し先をcontent_typeからcontent_type_with_charsetに切り替える1行のみで、ロジックの局所化が徹底されています。

また、MIME typeの比較には文字列ではなくMime::Typeオブジェクトを使用しており、Mime::Type.lookupで生成したオブジェクトを定数に格納してin?で判定する実装が採用されています。これにより、文字列比較に依存しない堅牢な判定が実現しています。

まとめ

本PRはPropshaft::ServerのRackレスポンスにcharset=utf-8を付与することで、Chromeの文字コード判定バグに起因するCSSの文字化けを防ぎます。ロジックをPropshaft::Assetに集約しつつ、対象MIMEタイプをRFC準拠の根拠に基づいて最小限に絞った、影響範囲を精密にコントロールした変更です。

記事メタデータ

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

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)やPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Railsの内部実装に関する知識を前提としており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクションが「総論→各論」で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、提供されたDiffの内容と完全に一致しています。メソッドのメモ化に関する言及など、Diffの細かい部分まで正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Content-Type, charset, Rack, MIME Type, RFCなど、関連する技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

Chromeのバグ、ActionDispatchとの差異、MIMEタイプ毎の対応理由など、変更の背景に関する技術的な説明がPR情報と一致しており、正確です。

事実の突合 ✓ PASS

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

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

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

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

PR番号(#264)、コンパニオンPR番号(#57188)、ChromiumのIssue番号など、記事内の数値や固有名詞はすべて正確です。

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

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

記事のタイトルはPRのタイトル「Add charset=utf-8 to Content-Type for CSS and HTML assets」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事内の情報はすべてPRで提供された範囲内にあり、バージョンサポート状況やリリース日程といったPR外の外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

「〜でした」「〜ようになりました」といった時間表現が、変更の前後関係を正しく反映しており、PRの内容と一致しています。