はじめに
API Gateway, AWS Lambda構成で、multipart/form-dataを使ってファイル送信をした際に、日本語ファイルが文字化けする事象に遭遇しました。その対応方法についてまとめます。
対象となる読者
multipart/form-dataを使って日本語ファイルを送信したが、ファイルが文字化けして困っている方
Lambda実行環境
- Node.js 18.x
- multipart/form-dataで送信したデータのバックエンド処置には nachomazzara/parse-multipart-dataを使用
結論
最初に結論だけ述べると、フロント側でファイル名をエンコードして、データ送信することで文字化けを回避することが出来ました。
バックエンドでの処理を模索しましたが、解決出来なかったため、もし解決方法ご存知の方がいらっしゃれば、コメントいただけると嬉しいです。
詳細
multipart/form-dataはデータ送信時のデフォルトcharsetはISO-8859-1
になるようです。multipart/form-data, what is the default charset for fields? の回答が参考になりました。ISO-8859-1は日本語がないため、この段階で文字化けが発生します。
そのためAPI gatewayにデータを送信する前に日本語ファイル名をencodeURIを使い、ISO-8859-1でエンコードされる前にUTF-8エンコードし、サーバー側で受け取ったデータをdecodeURIでデコードすることで文字化けなくファイル名を取得できました。
今回はクライアントサイドはJavascriptのFormDataとFetch APIを使って処理しました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Form Test</title>
</head>
<body>
<form>
<div>
<label for="file_name">Choose file to upload</label>
<input type="file" id="file_name" name="file_name" multiple />
</div>
<button type="submit">Submit</button>
</form>
<script>
const form = document.querySelector('form')
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(form)
const fileInput = document.getElementById("file_name")
const [file] = fileInput.files; // File APIの使用
const fileName = file.name
const fileExtention = fileName.substring(fileName.lastIndexOf(".") + 1);
const blob = file.slice(0, file.size, file.type);
const encodedFileName = encodeURI(file.name.substring(0, fileName.lastIndexOf("."))); //エンコードしたファイル名の取得
const renamedFile = new File([blob], encodedFileName + "." + fileExtention, {type: file.type});
formData.set('file_name', renamedFile)
fetch("https://sample.com", {
method: "POST",
mode: 'cors',
body: formData
})
.then(res => res.json())
})
</script>
</body>
</html>
ファイル名の取得にはFile APIを使用しました。
Fetch APIを使ってデータを送信する場合、小さなハマりどころがありました。
multiple/form-dataはMIME-TYPEの指定と、そのパラメータとしてboundaryの指定が必須です。Hypertext Transfer Protocol -- HTTP/1.1より。
そのためFetch APIでのリクエストは以下のように設定しました。
// NG
const formData = new FormData(form)
fetch("https://sample.com", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data"
},
body: formData
})
上記のように指定した場合、サーバー側で Content-Type: multipart/form-data
が送られませんでした。 fetch - Missing boundary in multipart/form-data POSTのstack overflowに事象の詳細がまとめられています。
これについては、FormDataのMDNにも以下のように警告されていましたのでご注意ください。
警告: FormData を使用して、XMLHttpRequest または Fetch_API を使用して、 multipart/form-data の Content-Type で POST リクエストを送信する場合 (Files や Blob をサーバーにアップロードする場合など)、リクエストの Content-Type ヘッダーを明示的に設定しないでください。そうすると、ブラウザーがリクエスト本文のフォームフィールドの区切りに使用する境界の表現で Content-Type ヘッダーを設定することができなくなります。
// OK
const formData = new FormData(form)
fetch("https://sample.com", {
method: "POST",
body: formData
})
Node.jsを使って、Lambda側でのデータの受け取りは以下のように処理をしました。
Lambdaでnode_modulesを使う場合は、レイヤーにzipファイルを追加する必要があります。レイヤーにnode_modulesを追加する方法はLambdaレイヤーにnode_modulesを登録してみたを参考にしました。
const multipart = require('parse-multipart-data')
module.exports.handler = async(event) => {
const encodeBody = event['body-json']; // eventからエンコードされたmultipart/form-dataの中身を取得
const header = event.params.header
const decodeBody = Buffer.from(encodeBody.toString(), "base64")
const boundary = multipart.getBoundary(header['Content-Type'])
const parsedBody = multipart.parse(decodeBody, boundary)
let fileName, type, content
for (const formdata of parsedBody) {
if (formdata["name"] == "file_name") {
fileName = decodeURI(formdata["filename"]);
type = formdata["type"]
content = Buffer.from(formdata["data"], "base64").toString("base64")
}
}
}