CSS・HTMLアセットのContent-Typeにcharset=utf-8を追加
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::Assetにcontent_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/cssとtext/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準拠の根拠に基づいて最小限に絞った、影響範囲を精密にコントロールした変更です。