Node.jsのfetchでバイナリデータをダウンロードする

Node.jsのv18でデフォルトで利用可能になったFetch APIを使用して、
バイナリデータをダウンロードする方法に苦労したため、備忘録としてこの記事に残す。

## バイナリデータをダウンロードする方法

`res.arrayBuffer()`メソッドの使用が正解。
ただし、ArrayBuffer型はNode.jsで上手く扱えないため、
`Buffer.from()`を挟んでバッファ型へ変換する必要がある。

```js
const fs = require("fs");

const main = async () => {
  const res = await fetch("https://example.com/path/to/image.png");

  // バイナリデータはarrayBufferメソッドを叩いて取り出す
  const arrayBuffer = await res.arrayBuffer();

  // Uint8ContentsのArrayBuffer型 -> Buffer型に変換
  const buffer = Buffer.from(arrayBuffer);
  fs.writeFileSync("path/to/image.png", buffer);
}
main();
```

もしStreamを使う場合この回答のやり方も考慮に入れて良さそうだが、
Streamはasync/awaitと相性が悪いので逐次処理等をいれるとコードが長くなりそうなのが難点
[Fetch blob from URL and write to file - stack overflow](https://stackoverflow.com/a/58761276)

回答を元にコードを書くとこんな感じの処理になるだろう。
※動作は未検証

```js
const fs = require("fs");

const main = async () => {
  const res = await fetch("https://example.com/path/to/image.png");
  const dest = fs.createWriteStream("path/to/image.png");
  res.body.pipe(dest);
}
```

## 追記: Responseオブジェクトにblobメソッドが存在しない理由

Node.jsドキュメントでfetchを探すと下記の記載がある

> A browser-compatible implementation of the [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/fetch) function.
> 意訳: fetch関数はブラウザー互換の実装です

では何故arrayBufferメソッドのみで、blobメソッドが存在しないのか？

MDNにあるBlogの記事を読めばわかるのだが、
BlobはRawデータの塊を一つのファイルとみなしたような存在のクラスである。
なので「MIMEタイプ」もあるし「URL.createObjectURL()」でURLに変換もできる。
BlobがWeb上で`img`タグの`src`に配置されるだけのことはある。

という訳で、Blobの用途は、Node.jsのBufferとは全く違うものである。
用途が違えば所持しているメソッドも変わってくる。
だからNode.jsでfetch APIを飛ばした結果としてBlobを作る必要性はない。

[Class: Blob | Node.js documentation](https://nodejs.org/api/buffer.html#class-blob)
いや、あんのかい！
まぁいいや、Node.jsで使うならBuffer型である方が都合がよく、
別にBlob型で得られても不便なだけだから無いのだろう。
これが`res.arrayBuffer()`は存在するが、`res.blob()`は存在しない理由だと推測される。

追記した2023/03/22現在は下記のステータスのままである。

> Stability: 1 - Experimental. Disable this API with the --no-experimental-fetch CLI flag.

やはりまだまだ議論されている段階のようだ
今後もしかすると`res.binary()`や`res.blob()`を実装する流れになる可能性もある。

参考記事

- [Globals > fetch | Node.js documentation](https://nodejs.org/api/globals.html#fetch)
- [fetch() | MDN](https://developer.mozilla.org/ja/docs/Web/API/fetch)
- [Blob | MDN](https://developer.mozilla.org/ja/docs/Web/API/Blob)
- [Class: Blob | Node.js documentation](https://nodejs.org/api/buffer.html#class-blob)

## ハマった原因

バイナリデータをfetchでダウンロードした場合、
`res.text()`ではテキストデータに変換されてしまう。
なのでバイナリデータになるメソッドを叩かなければならない。

しかしNode.jsのドキュメントの整備が追いついていない(?)
Node.js公式サイトから`await fetch(url)`で取り出した`res`型の詳細を確認することができなかった。
(このURLにあるよって情報を知ってたらコメントで教えてください)

なので手探りでメソッド名を探す必要がある。
[Fetch blob from URL and write to file - stack overflow](https://stackoverflow.com/questions/51444927/fetch-blob-from-url-and-write-to-file)
この回答者達は`res.binary()`や`res.blob()`メソッドを使えとアドバイスしてくれるが、そんなメソッドはなく`undefinedは関数ではないので実行できませんよ`という無慈悲なエラーで叩き落とされてしまう。

Node.jsには長らくFetch APIが存在していなかったので、
各ライブラリの開発者はデフォルトで提供されている`http`モジュール等を利用して
「オレオレFetch API」をでっちあげていたというわけだ。

そのオレオレFetch APIの一つの`node-fetch`の使い方の解説をいくら漁っても
Node.jsのv17〜v18で追加されたFetch APIと仕様が違っていては動かないに決まっている。
これがハマった原因だ。

## 調査の流れ

Node.jsのv18のリリース関連で記事を漁ってみた。

- [Fetch APIがデフォルトで利用できるNode.js v18 | TECH+](https://news.mynavi.jp/techplus/article/20220421-2326768/)
- [「Node.js 18」がリリース ～fetch API、Web Streams APIがグローバルスコープで利用可能に - 窓の杜](https://forest.watch.impress.co.jp/docs/news/1404310.html)
- [Fetch APIがデフォルトで利用できるNode.js v18：マピオンニュース](https://www.mapion.co.jp/news/column/cobs2404365-1-all/)
- [The Fetch API is finally coming to Node.js - LogRocket Blog](https://blog.logrocket.com/fetch-api-node-js/)

この内Node.jsは[undici](https://undici.nodejs.org/#/)ライブラリの実装を参考にFetch APIを導入しているという内容が複数の記事から得られた。

[Body Mixins](https://undici.nodejs.org/#/?id=body-mixins)にundiciの使い方が載っているが、
`res.body`にあるメソッドは`json`と`text`のみ
なんでバイナリが無いんだよ使えねえなこいつ。
GitHubのundiciのソースコードを漁ってみたがarrayBufferメソッドの形跡をつかめず。

すったもんだでたどり着いたのがMDNの[Response.arrayBuffer()](https://developer.mozilla.org/ja/docs/Web/API/Response/arrayBuffer)
Fetch API準拠ならarrayBufferを所持していてもおかしくない。
~~じゃあなんでblobメソッド持ってないんだよ~~
これで試した所、Uint8Contentsとして取得でき、保存したPNG画像を開いて画像の中身を確認できた。

undiciもスカとかもう二度とこの情報にたどり着ける気がしない……

## おまけ

v18.2.0現在、デフォルトで利用出来るようになったとはいえ、
何も指定せずに使うとこんなwarningメッセージが表示される。

```bash
$ node fetchが含まれているファイル.js
(node:2254573) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
```

v19系のlatestではwarningメッセージが消えた。
これで心置きなく利用できる。
`node-fetch`等を無理に利用する必要はなくなった。

## おまけ２: ホンマにunditi使ってんの？

2024/12/09現在、Node.jsでスクリプトをガチャガチャ動かしているとエラー吐いて停止した

```
node:internal/deps/undici/undici:11020
            fetchParams.controller.controller.error(new TypeError("terminated", {

TypeError: terminated
    at Fetch.onAborted (node:internal/deps/undici/undici:11020:53)
    at Fetch.emit (node:events:518:28)
    at Fetch.terminate (node:internal/deps/undici/undici:10178:14)
    at Object.onError (node:internal/deps/undici/undici:11141:38)
    at Request.onError (node:internal/deps/undici/undici:2094:31)
    at Object.errorRequest (node:internal/deps/undici/undici:1591:17)
    at TLSSocket.<anonymous> (node:internal/deps/undici/undici:6263:16)
    at TLSSocket.emit (node:events:530:35)
    at node:net:343:12
    at TCP.done (node:_tls_wrap:650:7) {
  [cause]: SocketError: other side closed
      at TLSSocket.<anonymous> (node:internal/deps/undici/undici:6238:28)
      at TLSSocket.emit (node:events:530:35)
      at endReadableNT (node:internal/streams/readable:1698:12)
      at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
    code: 'UND_ERR_SOCKET',
    socket: {
      localAddress: '240b:::::::1',
      localPort: 47392,
      remoteAddress: '2606:::::1',
      remotePort: 443,
      remoteFamily: 'IPv6',
      timeout: undefined,
      bytesWritten: 198,
      bytesRead: 38223
    }
  }
}

Node.js v22.11.0
```

本当にunditi使われてるんじゃん！知らなかった