署名付きURLを用いて、S3にファイルをアップロードした際に、なぜかアップロードしたファイルの中身だけでなく、変なヘッダーのようなものが付与された形で保存されている問題を解決した話です。
目次
- 署名付きURLとは?
- 発生した問題
- 原因
- 解決策
署名付きURLとは?
AWS S3ではバケット単位やIAMユーザ単位でアクセス制御を行うことができますが、その一環として、有効期限を設けてある特定のURLからアクセスできるようにする機能です。
本来なら、ブラウザからサーバー経由でファイルをアップロードして、S3バケットに保存するという流れが一般的かと思います。なのでサーバー側で、認証や分割ファイルの結合といった処理が行えます。
署名付きURL
と使うとサーバーを経由せず直接S3にアップロードできるため、運用しているサーバーに負荷をかけずに楽に運用できます。実装も楽です。特定のURLにアップロードすれば良く、前者に比べるとセキュリティに弱いため、有効期限を設定できます。
数GBの大きなファイルでも自社サーバーを経由せずに直接アップロードできるため、サーバーが落ちる心配がなく安心です。
発生した問題
PUT権限の署名付きURLにブラウザからJSライブラリの superagent
を用いてCSVファイルをアップロードしていたのですが、何故かS3にアップロードされたデータに意図しない変な情報が付与してしまいました。
id,name
1,pokohide
2,hogehoge
が署名付きURLにファイルをPUTすると、以下のような形式で保存されていました。
———WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="message"
id,name
1,pokohide
2,hogehoge
------WebKitFormBoundaryO5quBRiT4G7Vm3R7
実際にアップロードに際し使っていたコードの例がこちら
import request from 'superagent'
...
request
.put(presigned_url)
.attach('data', file)
.('progress', (e) => { console.log(e) })
.end((err, res) => {
if (err) console.log(err)
else console.log(res)
})
上記のコードの file
はINPUTから入力のあったファイルデータをそのまま渡していました。これで行けると思っていたのに・・・
原因
署名付きURLでファイルをアップロード(PUT)すると、S3側はPUTされた HTTP Body
の中身をそのまま保存しているそうです。
なので、PUTする前に HTTP Body
がどのタイミングで意図しない形式になっているかを調査しました。結果、原因はFormData APIにありました。
ブラウザが進化するにつれて複数の種類(csv
や image
など)のデータを一度に扱える形式(multipart/form-data
)が生まれ、この形式でフォーム送信された場合は内部的にHTTP Bodyが FormData
オブジェクト形式で送信されます。
この際に、もともと複数ファイルに送れるように拡張された形式であるため、今回のように一つのファイルを送信するときでも勝手にHTTPボディに
———WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="message"
{中身}
------WebKitFormBoundaryO5quBRiT4G7Vm3R7
というように、意図した中身だけでなく、サーバー側でに複数ファイルが送られたときに判別できるように区切り用の識別子のようなものが付与されてしまっていました。
今回の問題は、このようにHTTPボディが勝手に改変されて、それがそのままS3に保存されていたために起きていた問題であることが判明しました。
また、もともと使っていた superagent
というJS製のHTTPクライアントがFormData
のラッパーであったため、何をしようにもContent-Type: multipart/form-data
になってしまっていたことで上記の問題が引き起こされていました。なので、このライブラリとFormDataを使うことを諦め、 XMLHttpRequest でファイルをアップロードすることにしました。
※ superagent
でも Content-Type
を multipart/form-data
以外に変えれば上手く行ったかも知れませんが検証しませんでした。
参考URL
解決策
...
let xhr = new XMLHttpRequest()
xhr.open('PUT', url, true)
xhr.setRequestHeader('Content-Type', file.content_type)
xhr.setRequestHeader('X-FILE-NAME', file.name)
xhr.onload = function(e) {
if (this.readyState === 4) { // DONEを検知
console.log('upload done')
}
}
xhr.onerror = function(e) {
return reject(e)
}
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) { console.log('pregress') }
}
xhr.send(file)
このように、Content-Type
を multipart/form-data
ではなく、アップロードするファイルの Content-Type
をヘッダーに与えて、送信すればHTTPボディが変わることなく、意図した形式でデータがS3にアップロードされました。