Node.jsのv18でデフォルトで利用可能になったFetch APIを使用して、
バイナリデータをダウンロードする方法に苦労したため、備忘録としてこの記事に残す。
バイナリデータをダウンロードする方法
res.arrayBuffer()
メソッドの使用が正解。
ただし、ArrayBuffer型はNode.jsで上手く扱えないため、
Buffer.from()
を挟んでバッファ型へ変換する必要がある。
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
回答を元にコードを書くとこんな感じの処理になるだろう。
※動作は未検証
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() 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
いや、あんのかい!
まぁいいや、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
- fetch() | MDN
- Blob | MDN
- Class: Blob | Node.js documentation
ハマった原因
バイナリデータをfetchでダウンロードした場合、
res.text()
ではテキストデータに変換されてしまう。
なのでバイナリデータになるメソッドを叩かなければならない。
しかしNode.jsのドキュメントの整備が追いついていない(?)
Node.js公式サイトからawait fetch(url)
で取り出したres
型の詳細を確認することができなかった。
(このURLにあるよって情報を知ってたらコメントで教えてください)
なので手探りでメソッド名を探す必要がある。
Fetch blob from URL and write to file - stack overflow
この回答者達は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+
- 「Node.js 18」がリリース ~fetch API、Web Streams APIがグローバルスコープで利用可能に - 窓の杜
- Fetch APIがデフォルトで利用できるNode.js v18:マピオンニュース
- The Fetch API is finally coming to Node.js - LogRocket Blog
この内Node.jsはundiciライブラリの実装を参考にFetch APIを導入しているという内容が複数の記事から得られた。
Body Mixinsにundiciの使い方が載っているが、
res.body
にあるメソッドはjson
とtext
のみ
なんでバイナリが無いんだよ使えねえなこいつ。
GitHubのundiciのソースコードを漁ってみたがarrayBufferメソッドの形跡をつかめず。
すったもんだでたどり着いたのがMDNのResponse.arrayBuffer()
Fetch API準拠ならarrayBufferを所持していてもおかしくない。
じゃあなんでblobメソッド持ってないんだよ
これで試した所、Uint8Contentsとして取得でき、保存したPNG画像を開いて画像の中身を確認できた。
undiciもスカとかもう二度とこの情報にたどり着ける気がしない……
おまけ
v18.2.0現在、デフォルトで利用出来るようになったとはいえ、
何も指定せずに使うとこんなwarningメッセージが表示される。
$ 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
等を無理に利用する必要はなくなった。
おまけ2: ホンマに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使われてるんじゃん!知らなかった