Node.jsの組み込みFetchとそのUndici基盤の解析
Ethan Miller
Product Engineer · Leapcell

はじめに
長年、Node.jsでHTTPリクエストを行うには、axiosやnode-fetchのような外部ライブラリがよく使われてきました。これらのライブラリはコミュニティに貢献してきましたが、追加の依存関係と、ブラウザのネイティブfetch APIに慣れた開発者にとっては多少の学習曲線をもたらしました。Node.js 18のリリースにより、重要なマイルストーンが達成されました。グローバルで組み込みのfetch APIが、ブラウザのそれに似た形で、外部インストールの必要なく利用可能になったのです。この統合はNode.jsでのWeb開発を劇的に簡素化し、データ取得のために馴染みのあるインターフェースを提供し、パフォーマンスと安定性の向上を約束します。この記事では、Node.jsのネイティブfetchを徹底的に探求し、その基盤となるアーキテクチャを明らかにし、特にそれを支える最先端のHTTP/1.1クライアントであるundiciとの深い関係を調査します。
コアコンセプトの解説
fetchとundiciの詳細に入る前に、私たちの議論の中心となるいくつかの基本的な用語を明確にしましょう。
fetchAPI: ネットワークリクエストを行うためのモダンでPromiseベースのAPIで、ネットワーク経由でリソースを取得するためによく使用されます。XMLHttpRequestよりも柔軟で強力になるように設計されています。undici: Node.js向けの高性能なWHATWGfetchAPI互換HTTP/1.1クライアントで、高速で信頼性の高いものとしてゼロから構築されました。Node.jsの標準HTTPクライアントになることを目指しています。- WHATWG 
fetch標準: Web Hypertext Application Technology Working Group (WHATWG) によるfetchAPIを定義する仕様。Node.jsの組み込みfetchは、この標準に密接に準拠することを目指しています。 - Streams: チャンクでデータを処理するためのNode.jsの基本的な概念。
fetchはリクエストボディとレスポンスボディの両方でストリームを活用し、大量のデータを効率的に処理できるようにします。 - Request/Responseオブジェクト: 
fetchAPIによって使用されるコアオブジェクト。Requestオブジェクトは送信中のネットワークリクエストを表し、Responseオブジェクトは受信中のネットワークレスポンスを表します。 
Node.js Fetchの内部構造
Node.jsの組み込みfetch APIは、完全に独立した実装ではありません。むしろ、undiciライブラリの直接的な再エクスポートとわずかな変更です。Node.jsコアチームによるこの戦略的な選択は、いくつかの利点を提供します。
- 標準準拠: 
undiciはWHATWGfetch標準に準拠するように細心の注意を払って設計されており、Node.js開発者がブラウザ環境と一貫した動作をするAPIを得られるようにします。これにより、認知負荷が軽減され、アイソモーフィックコードが容易になります。 - パフォーマンスと効率: 
undiciはその卓越したパフォーマンスで知られています。カスタムリクエスト/レスポンスパーサー、コネクションプーリング、パイプライン、その他の最適化を備えており、古いHTTPクライアントと比較してオーバーヘッドを大幅に削減し、スループットを向上させます。undiciを活用することで、Node.jsfetchはこれらのパフォーマンス上の利点を継承します。 - 活発な開発: 
undiciは活発に開発されているプロジェクトであり、継続的な改善、バグ修正、機能強化が保証されています。それを統合することで、Node.jsはこれらの進歩を迅速に採用できます。 
基本的な使用例
Node.js 18+でのfetchの使用は、ブラウザと同程度簡単です。
// my-data-fetcher.js async function fetchData(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // または .text(), .blob(), .arrayBuffer(), .formData() console.log('Fetched data:', data); return data; } catch (error) { console.error('Error fetching data:', error); throw error; } } // 使用例: fetchData('https://jsonplaceholder.typicode.com/todos/1') .then(data => console.log('Successfully retrieved:', data)) .catch(error => console.error('Failed to retrieve:', error.message)); // POSTリクエストとカスタムヘッダーを使用した例 async function postData(url, data) { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('POST successful:', result); return result; } catch (error) { console.error('Error posting data:', error); throw error; } } postData('https://jsonplaceholder.typicode.com/posts', { title: 'foo', body: 'bar', userId: 1, }) .then(data => console.log('New post created:', data)) .catch(error => console.error('Failed to create post:', error.message));
このコードを実行するには、my-data-fetcher.jsとして保存し、ターミナルでnode my-data-fetcher.jsを実行するだけです。データが取得され、コンソールにログが表示されるのがわかります。
undiciがfetchを支える仕組み
Node.js 18+でglobal.fetch()を呼び出すと、実質的にはundici.fetch()が呼び出されています。統合はシームレスです。undiciはコアのHTTPクライアント機能を提供し、コネクション管理、リクエストのシリアライゼーション、レスポンスの解析、エラー処理を行います。
概念的なフローを簡略化して見てみましょう。
fetch呼び出し:fetch(url, options)が呼び出されると、グローバルなfetch関数(undici.fetch)がこれらの引数を受け取ります。Requestオブジェクトの作成:undiciは、提供されたURLとオプションに基づいてRequestオブジェクトを内部的に構築し、WHATWGfetch仕様に準拠します。- コネクション管理: 
undiciは洗練されたコネクションプーリングメカニズムを使用します。既存のHTTP/1.1コネクションを再利用するか、効率的に新しいコネクションを確立します。 - リクエスト送信: 
RequestオブジェクトはHTTPメッセージにシリアライズされて、確立されたコネクション経由で送信されます。undiciは、ヘッダー、ボディのエンコーディング、リダイレクトなどの懸念事項を処理します。 - レスポンス受信と解析: サーバーからのレスポンスを受信すると、
undiciは受信したHTTPメッセージを迅速に解析し、Responseオブジェクトを構築し、レスポンスボディをReadableStreamとして利用可能にします。 Responseオブジェクトの返却:fetch呼び出しは、Responseオブジェクトで解決され、status、headersのようなプロパティや、ボディを消費するためのjson()やtext()のようなメソッドにアクセスできます。これらのボディメソッドは、多くの場合、基盤となるundiciストリームの消費を伴います。
高度なfetch機能とundiciの役割
undiciは、fetchだけでなく、より低レベルのAPIも公開しており、HTTPクライアントの動作を直接制御する必要がある場合にアクセスできます。例えば、undici.Agentは、コネクションプーリング、タイムアウト、リダイレクトを細かく制御できます。
global.fetchで十分な場合が多いですが、カスタムコネクションエージェントや高度なプーリング戦略が必要なシナリオでは、直接undiciを使用することが検討されるかもしれません。しかし、fetch API自体がinitオブジェクト内で、以下のようなほとんどの一般的なユースケースをカバーする堅牢なオプションセットを提供しています。
signal:AbortControllerを使用してリクエストを中止します。const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒後に中止 fetch('https://slow-api.example.com/data', { signal: controller.signal }) .then(response => { clearTimeout(timeoutId); return response.json(); }) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.error('Fetch aborted by user or timeout'); } else { console.error('Fetch error:', err); } });redirect: リダイレクト動作を制御します(follow、error、manual)。- Streamsを使用した
body: 全てのペイロードをメモリにバッファリングすることなく、大量のデータを効率的に送信します。const { Readable } = require('stream'); async function uploadStreamData() { const readableStream = new Readable({ read() { this.push('Hello '); this.push('World!'); this.push(null); // データなし } }); try { const response = await fetch('https://httpbin.org/post', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: readableStream // Node.js fetchはReadableストリームを直接受け入れることができます }); const data = await response.json(); console.log('Stream upload response:', data); } catch (error) { console.error('Stream upload error:', error); } } uploadStreamData(); 
undiciのおかげで、Node.jsのfetch APIはこれらの高度なメカニズムをサポートしており、サーバーサイドアプリケーションでのネットワーク通信において強力で汎用的なツールとなっています。
結論
Node.js 18+へのfetch APIの統合は、開発者にHTTPリクエストを行うための、馴染みがあり、強力で、パフォーマンスの高い方法を提供し、プラットフォームにとって大きな進歩となります。このシームレスな体験は、Node.jsネイティブfetchのバックボーンとして機能する最先端のHTTP/1.1クライアントであるundiciのおかげです。undiciを活用することで、Node.js fetchは高パフォーマンスとWHATWG fetch標準への厳密な準拠を達成するだけでなく、モダンなWeb開発のための堅牢な基盤を提供します。Node.js fetchはHTTPのやり取りを単純化し、開発者がアプリケーションロジックに、クライアントサイドのHTTPの複雑さにとらわれずに、より集中できるようになります。