API Gateway経由でS3への画像ファイルアップロードが上手くいかない場合の対策

API Gateway経由でS3への画像ファイルアップロードが上手くいかない場合の対策

現象

ReactからAPI Gateway経由でファイルをS3にアップロードしようとしたが、CORSエラーとなり正常終了しない。

対策

  • CORS設定を見直してlocalhostを許可する設定にする
  • バイナリメディアタイプ をワイルドカードにしない

状況

https://aws.amazon.com/jp/premiumsupport/knowledge-center/api-gateway-upload-image-s3
上記を参照してAPI GatewayからAmazonS3にファイルをアップロードできるように準備をしておきました。
上記ではPUTメソッドでリクエストすることになっています。

Reactではpropetyでフォームから受け取ったFileオブジェクトを受け取りfetchでPUTしています。

/* 前略 */        
if(props.file){
    fetch(url,{
        method:'PUT',
        headers:{
            'Content-Type':props.file.type
        },
        body:props.file
    }).then(res=>{
        console.log(res)
    })
}
/* 後略 */

これを実行し、開発者ツールで結果を確認すると下図のようになっていました。

Access to fetch at ‘https://xxxxxxxxxxxx.ap-northeast-1.amazonaws.com/xxxxxx/xxxxxxxx/xxxxxx.jpg’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

これはサーバー側のCORS設定がおかしいときに度々表示されるエラーなので、遭遇するのは初めてではありません。

一方ネットワークタブを確認すると下図のようになっていました。

CORSエラーはコンソールの表示内容と同じですが、同ファイルでもう1行表示があり、こちらはタイプがpreflightとなっていました。
(2ファイル表示されているのはここでは気にしないでください)

また、Reactからアクセスするとエラーになりますが、Postmanを使ってAPIに直接アクセスした場合は正常に処理ができるという状況だったのも疑問でした。

観点1:CORS設定の確認

CORSについては検索すればたくさん情報が出てくるので詳細は説明しません。
ざっくりいうと「許可しない変なところからアクセスされた場合にAPIを動作させないための仕組み」です。

今回で言うと ‘http://localhost:3000’というのがAWSから見て許可していないアクセス元だったので、エラーを返しています。

というわけでまず1点目として、API GatewayのCORS設定を正して、localhostからでもAPIにアクセスできるようにします。

こちらもやり方は検索すればたくさん出てくるので詳細は説明しません。
統合レスポンスのレスポンスヘッダーに[Access-Control-Allow-Headers] [Access-Control-Allow-Methods] [Access-Control-Allow-Origin]が設定できていれば良いと思います。

今までは、CORSでエラーになった場合はこの対策で解決していましたが、今回はこの対策だけでは改善しませんでした。

観点2:バイナリファイルの設定

結論を先に書いておくと、API GatewayのAPI->設定->バイナリメディアタイプが[*/*]となっていたのが問題で、[image/*]のように、アップロードする画像に合わせて限定したら正常に動作するようになりました。

観点1のCORSを修正してもReactからのfetchではエラーが発生していましたが、PostmanでAPIをテストした場合は問題なく処理ができていました。
この差を調べるためにCloudWatchで実行ログを取得し、fetchだとどこでエラーが起きるのか?Postmanでの実行時との差は何なのか?を確認しました。
(例によってCloudWatchの使い方は検索してください)

ログからわかったことが2点ありました。

  • PostmanはPUTメソッドのみを実行しているのに対して、fetchではOPTIONSメソッドのリクエストが発行され、このリクエストの最中にエラーが発生している
  • エラーが発生しているのはbodyのバイナリデータの変換に失敗するため

まずOPTIONSメソッドですが、これこそが状況の項目で書いたpreflightです。preflight requestはCORSに係る通信の1つで、あらかじめ本番の通信(今回で言うとPUT)ができるかを確認するための通信です。preflightは発動する場合としない場合があり今回使っているPUTメソッドはpreflightの対象だそうです。

preflightはブラウザ固有の機能だそうで、postmanはサービスの中でpreflightを行っていなかったためにOPTIONSをスキップしてPUT処理を行っているのだと思われます。その結果、処理も問題なく行えたのでしょう。

以上からpreflightのバイナリデータの扱いに問題がありそうだと分かりました。preflightのrequestを詳細に確認すると、headersに’Content-Type’がないことに気付きました。この辺の仕様はあまり詳しくないので、そういうものなのか、fetchの呼び方がおかしいのかは不明です。

ただ、少なくともContent-Typeが不明な状態でバイナリファイルを渡されてもどう処理したらよいか分からず、サーバー側が困ってしまうのは想像ができます。

ネットを見ていると、 API GatewayのAPI->設定->バイナリメディアタイプを空欄にして、バイナリファイルに対応させなければ良いという解決策を見つけました。
しかし、今回はS3にファイルをアップロードするためのAPIなので、そうはいきません。

ということでいろいろ試した結果 API GatewayのAPI->設定->バイナリメディアタイプを[image/*] のようにワイルドカードではなく、適切にメディアタイプを入力してみたところ問題が解決しました。

この設定にすればOPTIONS(preflight)ではバイナリファイルに反応しなくなり、Content-Typeが設定されるPUTではバイナリファイルに正しく反応するようになります。