🖋️

Amazon S3でhtmlをブラウザ表示した際、一瞬文字化けする問題とその解消方法

2021/01/27に公開

概要

AWS CLIを用いてS3にアップロード・CloudFrontを使用し配信した静的サイトについて、
ブラウザでページを読み込んだ際に一瞬だけ文字化けし、その後すぐ正常に表示される、という症状に遭遇しました。
文字コード系のバグだということは割とすぐに分かりましたが、原因にたどり着くまで時間がかかってしまったので、やったことを記しておきます。

原因と修正した内容

直接的には、AWS CLIの aws s3 sync コマンドが原因でした。
以下が元々のコマンド

aws s3 sync ./out/ ${targetBucket} --exact-timestamps --delete --exclude ".DS_Store" --acl public-read

( ${targetBucket} は任意のS3バケット)

これを以下に変更したところ、症状は起こらなくなりました。

aws s3 sync ./out/ ${targetBucket} --exact-timestamps --delete --exclude ".DS_Store" --exclude "*.html" --acl public-read
aws s3 sync ./out/ ${targetBucket} --exact-timestamps --include "*.html" --content-type "text/html; charset=utf-8" --acl public-read

尚、aws-cliのバージョンは 2.1.15を使用しています。

一瞬文字化けが起こった理由

配信していたページのResponse headerをChromeのデベロッパーツールから確認すると、
Content-Type の指定が text/html のみで charset の指定がない状態で配信されていました。

そのため、htmlのmetaタグに示された以下の内容がブラウザに解釈されるまで一瞬間があり、
正しくエンコードされるまでの間文字化けした表示になっていたようです。

<meta charset="utf-8" />

AWS CLIのContent-Type指定について

AWS CLIで sync コマンドを叩く際、ユーザーからの指定が特にない場合、
Python標準ライブラリの mimetypes モジュール によってContent-Typeが判断されるようでした。
例えば以下のようなイメージ。

$ python
Python 3.7.4 (default, Sep  8 2019, 19:04:29)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import mimetypes
>>> mimetypes.guess_type('./out/index.html')
('text/html', None)

この場合 text/html と判断されていますが、
今回の問題を解決するためにはcharset指定もして欲しいところです。
そこでhtmlの場合のみ text/html; charset=utf-8 となるよう、コマンドを変更します。

Content-Typeの手動指定

aws s3 sync コマンドは --content-type オプションを用いることで、任意のContent-Typeを指定することができます。
そこで、元々一括で全ファイルを同期させていたものを
.htmlファイルとそれ以外のファイルで2回に分けてコマンドを走らせることで、
.htmlファイルには --content-type "text/html; charset=utf-8" の指定を、
それ以外のファイルは今まで通り自動で付与されるcontent-typeを使用するように変更します。

以下のようなコマンドを流すシェルスクリプトを書き、デプロイ時はそれを実行するようなフローとしました。

aws s3 sync ./out/ ${targetBucket} --exact-timestamps --delete --exclude ".DS_Store" --exclude "*.html" --acl public-read
aws s3 sync ./out/ ${targetBucket} --exact-timestamps --include "*.html" --content-type "text/html; charset=utf-8" --acl public-read

1つめのコマンドでは --exclude "*.html" とすることでhtml以外のファイルをsyncしておき、
2つめのコマンドでは逆に --include "*.html" と指定することでhtmlのみに--content-type "text/html; charset=utf-8"を適用させるようにしています。

その他、確認時の注意点

修正したコマンドでデプロイを実行しても、すぐにはブラウザ画面には反映されませんでした。
これは、CloudFront側でキャッシュを持っていたためと思います。
その場合は、キャッシュが切れるか、CloudFrontの側で強制的にファイルを取得させてやるなどすれば更新されます。

今回はそこまで長いキャッシュを設定していなかったので、しばらく放置してから確認すると
content-type: text/html; charset=utf-8 のヘッダ情報の反映と共に、文字化けが起こらなくなることも確認できました。

参考

Unable to set `charset` when mime-types are guessed (S3) · Issue #1346 · aws/aws-cli
AWS CLIでS3オブジェクトのContentTypeを設定する – Siguniang's Blog

Discussion