AWSのS3でREST APIエンドポイントを使う場合、ファイル名(キー)にプラス記号 +
(U+002B)を含めてリクエストすると、スペース(U+0020)に変換されてしまいます。そして悲しいことに、この挙動はCloudFrontでS3オリジンを使った際にも適用されます。ただし、S3 Static Website Hostingを使った際にはこの挙動になりません。
具体例
バケットに以下のファイルがあるとします。バケット名は仮に example
とします。
test+1.txt
test 1.txt
S3に直接アクセス
https://example.s3.ap-northeast-1.amazonaws.com/test+1.txt
を開くと、test 1.txt
のほうが表示されます。
ウェブサイトエンドポイントにアクセス
S3 Static Website Hostingを有効化して
http://example.s3-website-ap-northeast-1.amazonaws.com/test+1.txt
を開くと、正常に test+1.txt
が表示されます。
CloudFrontを通してアクセス
通常通りオリジンをS3にしてディストリビューションを作成して
http://example.cloudfront.net/test+1.txt
を開くと、 test 1.txt
のほうが表示されます。
CloudFrontを通してアクセス(ウェブサイトエンドポイント)
オリジンのバケットにStatic Website Hostingが設定されていると、こんなメッセージが出てきます。
「Webサイトのエンドポイントを使用」をクリックするとフォームが自動的に example.s3-website-ap-northeast-1.amazonaws.com
のように更新されます。
この状態で同じURL
http://example.cloudfront.net/test+1.txt
を開くと、正常に test+1.txt
が表示されます。
何が困るの?
ファイル名に +
が含まれるファイルを取得する際に、%2B
に変換してやれば表示はできます。どう変換すれば良いかは「オブジェクトURL」に記載があります。
しかし、S3以外のほぼすべてのWebサーバはこんな挙動はせず、+
のままでアクセスできます。encodeURI()
もパス部分に含まれる+
はそのままですし、URL標準でもパス部分で変換するという記述は見つかりません。
これが単一ファイルをWeb公開するだけならアクセスする人が気をつけるでも良いのですが、実際のWebサイトで
<img src="a+b.png">
のようにファイル名に+
を含む画像を参照するHTMLがあったりすると、リンク切れしてしまいます。既存資産でそれほど珍しいものではないでしょう。WebサイトをS3に移行できない!!
ウェブサイトエンドポイントを使えば?
それができれば良いのですが、ウェブサイトエンドポイントではOrigin Access Control(OAC)が使えません。
この設定が消えてしまいます。
公式サポートでもこれを根拠に非対応とのこと。
ウェブサイトエンドポイント
公開で読み取り可能なコンテンツのみをサポートします。
ウェブサイトエンドポイント - Amazon Simple Storage Serviceより
なので、S3側へのアクセスを防いでCloudFront側で柔軟に設定したりCloudFront Functionsで認証をかけたり、という運用を想定しているなら、ウェブサイトエンドポイントは使えません。
回避策
ウェブサイトエンドポイントの使用は諦めて、REST APIエンドポイントを使ったまま、CloudFront FunctionsでURLを書き換えるのが現実的でしょう。
適当な関数を作ります。
コードは雑にこのくらいで良いでしょう。
function handler(event) {
const request = event.request;
request.uri = request.uri.replaceAll('+', '%2b');
return request;
}
※ encodeURIComponent
で全部パーセントエンコーディングしてやっても良いのですが、先頭の/
は残さないといけない、クエリパラメータ部分はそのままにする、など考えることが多いです。今回の問題を塞ぐだけなら、1文字置換が一番カンタンです。
なお、 request.uri
は名前に反して「URIのうちパス部以降」となります。
リクエストされたオブジェクトの相対パス。
CloudFront Functions のイベント構造 - Amazon CloudFrontより
とありますがこれも不正確で、クエリパラメータも含まれます。例えば https://example.com/foo/bar.html?a=b#c
にアクセスしたら、 uri
には /foo/bar.html?a=b
がセットされます。
上記JSコードは手抜きなのでクエリパラメータ部分も変換されてしまいますが、今回クエリパラメータは要件に含めていないので気にしないことにします。
あとはビューアリクエストにセットしてやります。
当然ですが1個しか関連付けできないので、別の要件でビューアリクエストを使っているときは、既存のFunctionsやLambda@Edgeを書き換えて変換処理を入れてやる必要があります。
最後に
幸いCloudFront Functionsの料金は安く、100万回あたり $0.10 です。1億回実行されても10ドルなので、まあ許容範囲でしょう。
ただ、S3をオリジンにしてCloudFront経由でWebサイトを公開したい!というだけでこんな面倒が必要なのは少し予想外でした。ウェブサイトエンドポイントがOACに対応するとかでも良いので、そのうちに標準機能で出来たら嬉しいところです。