Node.js上からmultipart/form-data形式でHTTPリクエストをする

Node.jsからほかのサーバーにファイルを送るには、いろいろな方法がある。

- ファイルのURLを送り、対向側で別途ダウンロードしてもらう。
- ファイルをbase64形式にしてJSONのプロパティで送る
- multipart/form-data形式で送る
- JSONを使わない、HTTPリクエストを使わない

など？

## form-data を使って multipart/form-data で送信する

multipart/form-data形式で送る。これは、Webブラウザのformタグが伝統的にやっている方法と同じだ。Node.jsを使うなら、JSONなどを使うほうが楽だが、どうしても multipart/form-data 形式を使いたいケースもある。

ブラウザ側でmultipart/form-dataを使うのは簡単だ。ビルトインクラスのFormDataがある。

```ブラウザ.js
async onFileChanged(event) {
  const file = event.target.files[0]
:
  var form = new FormData();
  form.append("file", file);
  await axios.post(`/upload`, form);
:
```

Node.jsでは、[form-data](https://github.com/form-data/form-data) パッケージを使うと同じことができる。

https://github.com/form-data/form-data

ブラウザのFormDataと同じインターフェイスを目指して作られている。

また、ブラウザには無いヘルパーがある。簡単に送信できる`submit`メソッドなど。

```form-data.js
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
form.submit('http://example.com/upload', function(err, res) {
  console.log(res.statusCode);
});
```

テストするためにミニマルなサーバーコードを用意する。これには [multer](https://github.com/expressjs/multer)を使う。

```サーバー(express).js
:
const multer = require("multer");
const upload = multer({
  storage: multer.memoryStorage()
});
app.post("/upload", upload.single("my_file"), async (req, res, next) => {
  const { file } = req;
  if (!file) {
    res.status(500).send("NG");
    return;
  }
  console.log(file.mimetype);
  console.log(file.originalname);
  res.send("OK");
});
:

```

これは動く。

### Bufferを送ろうとすると動かない?

form-dataを使うときは、入力するデータの型に注意が必要。↑のサンプルのように、inputStreamを使う場合は問題無いが、Bufferなどを使うと、なぜか、送信できない。

正確には、送信できるが、サーバー側のmulterで処理できずに止まってしまう。


```.js
// サーバー側でエラーになり、うごかない
const FormData = require('form-data');
const form = new FormData();
:
const buffer = fs.readFileSync('image.jpg'); // Bufferに読み込む
form.append('my_file', buffer); // Bufferを渡す
form.submit('http://example.com/upload', function(err, res) {
  console.log(res.statusCode);
});
:
```

form-dataは、入力クラス次第で、ファイル名などの情報を自動的に推測してくれるが、ファイルのメタ情報が失われている場合、form.appendの第三引数で明示的に指定する必要がある。

```.js
:
const buffer = fs.readFileSync('image.jpg');
form.append('my_file', buffer, {
  filename: 'image.jpg'
  contentType: 'image/jpeg',
  knownLength: buffer.length
  });
:
```

ブラウザがどのように multipart/form-data でデータを送信しているのかを、dev tool などで観察すると、理解が深まる。ブラウザがファイルを送信するときには、HTTPリクエスト内の各サブパートに、Content-Dispositionヘッダをつけて、ファイル名などのメタ情報を書き込む。これが無いと、サーバー側では、ファイルとして認識できない。

```
Content-Disposition: form-data; name="file"; filename="my-image.jpg"
Content-Type: image/png

{ここに本文}
```

[MDN の Content-Disposition のドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition)


## axiosでFormDataを送る

form-dataパッケージは送信するための関数 .submit()を持っているが、ほかのHTTPリクエストクライアントでも送信することができる。axiosを例にすると、次のようなコードでできる。

```js
const form = new FormData();
:
:
const response = await axios.post(`http://example.com/upload`, form, {
  headers: form.getHeaders()
});
```
form.getHeaders()メソッドでHTTPヘッダを取得して、axiosに渡す必要がある点に注意が必要。これの中身は、次のようなContent-Typeヘッダの情報になっている。この boundaryの識別子を、HTTPヘッダに含める必要があるということ。

```
Content-Type: multipart/form-data; boundary=--------------------------709506743092431454880062
```