Tech Racho エンジニアの「?」を「!」に。
  • インフラ

Amazon S3ではプラス記号がスペースに変換される

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に対応するとかでも良いので、そのうちに標準機能で出来たら嬉しいところです。

関連記事

Lambda@Edgeを使うとき、デフォルトで作成されるIAMロールは権限が足りないので注意


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。