multipart/form-dataで、日本語ファイル名が文字化けする(API Gateway, Lambda構成)

### はじめに

API Gateway, AWS Lambda構成で、multipart/form-dataを使ってファイル送信をした際に、日本語ファイルが文字化けする事象に遭遇しました。その対応方法についてまとめます。

---

### 対象となる読者

multipart/form-dataを使って日本語ファイルを送信したが、ファイルが文字化けして困っている方

---


### Lambda実行環境

- Node.js 18.x
- multipart/form-dataで送信したデータのバックエンド処置には [nachomazzara/parse-multipart-data](https://github.com/nachomazzara/parse-multipart-data)を使用

---

### 結論
最初に結論だけ述べると、フロント側でファイル名をエンコードして、データ送信することで文字化けを回避することが出来ました。
バックエンドでの処理を模索しましたが、解決出来なかったため、もし解決方法ご存知の方がいらっしゃれば、コメントいただけると嬉しいです。

---

### 詳細
multipart/form-dataはデータ送信時のデフォルトcharsetは`ISO-8859-1`になるようです。[multipart/form-data, what is the default charset for fields?](https://stackoverflow.com/questions/4085449/multipart-form-data-what-is-the-default-charset-for-fields#answer-4086046) の回答が参考になりました。ISO-8859-1は日本語がないため、この段階で文字化けが発生します。

そのためAPI gatewayにデータを送信する前に日本語ファイル名を[encodeURI](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/encodeURI)を使い、ISO-8859-1でエンコードされる前にUTF-8エンコードし、サーバー側で受け取ったデータを[decodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI)でデコードすることで文字化けなくファイル名を取得できました。

今回はクライアントサイドはJavascriptの[FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects)と[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)を使って処理しました。

```index.html
<!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](https://developer.mozilla.org/en-US/docs/Web/API/File_API)を使用しました。

Fetch APIを使ってデータを送信する場合、小さなハマりどころがありました。

multiple/form-dataはMIME-TYPEの指定と、そのパラメータとしてboundaryの指定が必須です。[Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)より。
そのためFetch APIでのリクエストは以下のように設定しました。

```formdata.js
// 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](https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post)のstack overflowに事象の詳細がまとめられています。
これについては、FormDataのMDNにも以下のように[警告](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects#sending_files_using_a_formdata_object)されていましたのでご注意ください。

>警告: FormData を使用して、XMLHttpRequest または Fetch_API を使用して、 multipart/form-data の Content-Type で POST リクエストを送信する場合 (Files や Blob をサーバーにアップロードする場合など)、リクエストの Content-Type ヘッダーを明示的に設定しないでください。そうすると、ブラウザーがリクエスト本文のフォームフィールドの区切りに使用する境界の表現で Content-Type ヘッダーを設定することができなくなります。

```formdata.js
// 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を登録してみた](https://zenn.dev/mn87/articles/c421ebaea55f8b)を参考にしました。

```index.js
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")
    }
  }
}
```