15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2022-07-25

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()を実装する流れになる可能性もある。

参考記事

ハマった原因

バイナリデータを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のリリース関連で記事を漁ってみた。

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

Body Mixinsにundiciの使い方が載っているが、
res.bodyにあるメソッドはjsontextのみ
なんでバイナリが無いんだよ使えねえなこいつ。
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使われてるんじゃん!知らなかった

15
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?