← ブログに戻る

Node.jsにおけるUndici ProxyAgent:HTTP/SOCKS5プロキシの設定と依存関係なしのIPローテーション

Node.jsでHTTPおよびSOCKS5プロキシを使用するための組み込みのundici ProxyAgentの使い方、IPのローテーションの設定、https-proxy-agentやsocks-proxy-agentを使用せずに済む方法について解説します。

📅2026年10月18日

大多数のNode.jsに関するプロキシガイドは、プロジェクトにhttps-proxy-agent、socks-proxy-agentなどのいくつかのパッケージを追加することを勧めています。しかし、Node.js 18以降では、組み込みのHTTPクライアントundiciがすでにProxyAgentを含んでおり、プロキシを介したAPIの解析やテストの大部分のタスクをカバーしています。この記事では、undiciを介したHTTP/HTTPSトンネリングの設定方法、リクエスト間のIPローテーションの方法、追加の依存関係を持たずにSOCKS5が必要な場合の対処法について説明します。

なぜaxiosやrequestの代わりにundiciを使うのか

undiciは、Node.jsチームによって書かれたHTTP/1.1およびHTTP/2クライアントであり、18バージョン以降はグローバルなfetchの基盤となっています。トランジティブ依存関係はなく、接続プーリングに関するほとんどのベンチマークでaxiosよりも高速であり、私たちのテーマにとって重要なのは、HTTP CONNECTトンネリングを介してプロキシを操作するためのネイティブモジュールがあることです。

WildberriesやOzonを解析したり、異なる地域からの広告クリエイティブをテストしたりする際、node_modules内の余分なパッケージは、Node.jsの更新時に潜在的な障害点となります。undiciはランタイムと共に提供されるため、最新のTLS、HTTP/2、およびkeep-aliveの実装を取得でき、放置されたライブラリが新しいメジャーバージョンで壊れるリスクがありません。

インストールは非常に簡単です:

npm install undici

Node.js 20以上を使用している場合、一部のAPIはインポートなしでグローバルに利用可能ですが、ProxyAgentはデフォルトではグローバルfetchにエクスポートされていないため、インポートが必要です。

ProxyAgentの基本設定

プロキシを介したリクエストの最小限の動作例は次のようになります:

import { ProxyAgent, fetch } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

const data = await response.json();
console.log(data);

重要なポイントは、dispatcherパラメータです。これは、古いhttpモジュールのagentオプションではなく、undiciのディスパッチャーであり、TLSハンドシェイクの前に接続をキャッチし、CONNECTメソッドを介してターゲットホストへのトンネルを設定します。プロキシを介したHTTPSリクエストにとってこれは重要です: 通常のHTTPプロキシは暗号化されたトラフィックの内容を見ず、TCPトンネル内のバイトを単に通過させるだけです。

fetchではなく、低レベルのundici.requestを使用している場合、構文は同様です:

import { request, ProxyAgent } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const { statusCode, body } = await request('https://api.example.com/data', {
  dispatcher: proxyAgent,
});

console.log(statusCode);
for await (const chunk of body) {
  console.log(chunk.toString());
}

毎分数百のリクエストを行うプロダクションパーサーでは、各リクエストごとに新しいProxyAgentを作成しないことが重要です—これは接続プーリングを破壊します。接続プールのために一度エージェントのインスタンスを作成し、呼び出し間で再利用してください。

ユーザー名とパスワードによるプロキシ認証

ほとんどの有料プロキシプロバイダーは、IPホワイトリストではなく、ユーザー名/パスワードのペアでアクセスを提供します。undiciでは、認証情報はプロキシのURLに直接渡されます:

import { ProxyAgent, fetch } from 'undici';

const login = 'user123';
const password = 'pass456';
const proxyUrl = `http://${login}:${password}@gate.proxycove.com:9000`;

const proxyAgent = new ProxyAgent(proxyUrl);

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

console.log(await response.json());

注意: ログイン名やパスワードに特殊文字(@、:、/)が含まれている場合は、encodeURIComponentを通過させる必要があります。そうしないと、undici内のURLパーサーが接続文字列を正しく解析できず、理解しやすいメッセージの代わりに407認証エラーが発生します。

セッションの安定性が重要なタスク—たとえば、セッションの間に特定のIPにバインドされたマーケットプレイスでの認証—では、ステッキーセッションを持つレジデンシャルプロキシを使用する方が便利です: IPは指定された時間変更されず、サービスは地理間の「ジャンプ」を認識せず、各リクエストで再CAPTCHAを要求しません。

リクエスト間のプロキシローテーション

N個のプロキシのリストがあり、リクエストをそれらの間で分配する必要がある場合、最も簡単な方法は、サードパーティのキューライブラリなしのラウンドロビンです:

import { ProxyAgent, fetch } from 'undici';

const proxyList = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

let currentIndex = 0;

function getNextAgent() {
  const proxyUrl = proxyList[currentIndex];
  currentIndex = (currentIndex + 1) % proxyList.length;
  return new ProxyAgent(proxyUrl);
}

async function fetchWithRotation(url) {
  const agent = getNextAgent();
  return fetch(url, { dispatcher: agent });
}

for (let i = 0; i < 10; i++) {
  const res = await fetchWithRotation('https://httpbin.org/ip');
  console.log(await res.json());
}

このアプローチは、商品カードの解析や価格の監視に適しており、各次のリクエストは前のリクエストから論理的に独立しています。リクエストごとにIPを高速に切り替える必要があるタスクでは、プロバイダー側で自動ローテーションを持つプロキシを使用する方が簡単です—その場合、上記のすべてのコードは静的なゲートウェイのURLに集約され、ローテーションはデータセンタープロキシまたはタイマーによるローテーションを持つレジデンシャルプールが引き受けます。

単なる円形ローテーションではなく、重み付けされたローテーションが必要な場合—たとえば、より高速なプロキシに優先順位を与える場合—データ構造に重みを追加します:

const weightedProxies = [
  { url: 'http://user:[email protected]:8000', weight: 5 },
  { url: 'http://user:[email protected]:8000', weight: 1 },
];

function pickWeighted(list) {
  const total = list.reduce((sum, p) => sum + p.weight, 0);
  let rnd = Math.random() * total;
  for (const p of list) {
    if (rnd < p.weight) return p.url;
    rnd -= p.weight;
  }
  return list[0].url;
}

プロキシの生存チェックと「死んだ」IPの除外

プロキシプールは時間とともに「腐ります」: 一部のIPはターゲットサイトによって禁止され、一部は応答しなくなります。ローテーションが死んだアドレスを通過しないようにするために、組み込みのAbortControllerを介したヘルスチェックを追加します:

import { ProxyAgent, fetch } from 'undici';

async function isProxyAlive(proxyUrl, timeoutMs = 5000) {
  const agent = new ProxyAgent(proxyUrl);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch('https://httpbin.org/ip', {
      dispatcher: agent,
      signal: controller.signal,
    });
    return res.status === 200;
  } catch {
    return false;
  } finally {
    clearTimeout(timer);
  }
}

async function filterAliveProxies(proxyList) {
  const checks = await Promise.all(
    proxyList.map(async (url) => ({ url, alive: await isProxyAlive(url) }))
  );
  return checks.filter((c) => c.alive).map((c) => c.url);
}

このようなチェックを数分ごとにバックグラウンドタイマーで実行し、全体の静的リストではなく、アクティブプールを更新します。これは、安価なデータセンターIPで自分のプロキシを保持している場合に特に重要です—そこでは障害率が管理されたプロバイダープールよりも高くなります。

サードパーティライブラリなしのSOCKS5

重要な注意点: undiciの組み込みProxyAgentはHTTP CONNECTトンネリングを使用しており、「箱から出して」HTTP/HTTPSプロキシのみを理解します。SOCKS5プロトコルは異なり—これはTCPレベルでのバイナリハンドシェイクであり、undiciはネイティブにそれを解析しません。socks-proxy-agentをインストールせずにSOCKS5が必要な場合、netモジュールを介して手動で最小限のコネクタを実装し、カスタムAgentのconnectオプションを使用できます:

import net from 'node:net';
import { Agent, fetch } from 'undici';

function socksHandshake(socket, targetHost, targetPort) {
  return new Promise((resolve, reject) => {
    // ステップ1: 挨拶、認証なしのメソッド
    socket.write(Buffer.from([0x05, 0x01, 0x00]));

    socket.once('data', (greeting) => {
      if (greeting[1] !== 0x00) {
        return reject(new Error('SOCKS5: 認証メソッドはサポートされていません'));
      }

      const hostBuf = Buffer.from(targetHost, 'utf8');
      const request = Buffer.concat([
        Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
        hostBuf,
        Buffer.from([targetPort >> 8, targetPort & 0xff]),
      ]);

      socket.write(request);

      socket.once('data', (reply) => {
        if (reply[1] !== 0x00) {
          return reject(new Error('SOCKS5: 接続が拒否されました'));
        }
        resolve(socket);
      });
    });
  });
}

const socksAgent = new Agent({
  connect: (options, callback) => {
    const socket = net.connect(1080, 'socks-proxy.example.com', async () => {
      try {
        await socksHandshake(socket, options.hostname, 443);
        callback(null, socket);
      } catch (err) {
        callback(err, null);
      }
    });
    socket.on('error', (err) => callback(err, null));
  },
});

const response = await fetch('https://httpbin.org/ip', { dispatcher: socksAgent });
console.log(await response.json());

これは、ユーザー名とパスワードによる認証なしの簡略化されたハンドシェイク(メソッド0x02)であり、IPv6のサポートはありません—プロダクション用には、認証とさまざまなアドレスタイプの処理を追加する必要があります。しかし、メカニズムを理解するにはこれで十分です: SOCKS5はハンドシェイクの段階でトンネル内にTLSを必要とせず、HTTP CONNECTとは異なり、コードは見た目よりも短くなります。

実際には、SOCKS5はウェブページの解析ではなく、Dolphin AntyやAdsPowerのようなモバイルアプリケーションやアンチデテクトブラウザのトラフィックを通過させるために使用されることが多く、プロトコルはソケットレベルでの動作に必要です。Node.jsでのHTTP/HTTPSトラフィックが目的であれば、組み込みのProxyAgentで通常は十分であり、手動のSOCKS5コネクタに手を加える必要はありません。

タイムアウトの処理とリトライロジック

プロキシは、ダイレクト接続よりも接続を切断することが多いです—これは、ノードがいつでもオフラインになる可能性があるレジデンシャルネットワークでは正常です。指数的な遅延を持つリトライラッパーが必要です:

import { ProxyAgent, fetch } from 'undici';

async function fetchWithRetry(url, proxyList, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const proxyUrl = proxyList[attempt % proxyList.length];
    const agent = new ProxyAgent(proxyUrl);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 8000);

    try {
      const res = await fetch(url, { dispatcher: agent, signal: controller.signal });
      clearTimeout(timer);
      if (res.status >= 500) throw new Error(`サーバーエラー: ${res.status}`);
      return res;
    } catch (err) {
      clearTimeout(timer);
      lastError = err;
      const delay = 500 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error(`すべての試行が失敗しました: ${lastError?.message}`);
}

このラッパーは、エラー時のローテーションの問題も解決します: 現在のプロキシがダウンした場合、次の試行は自動的にリスト内の別のIPに移動し、同じ死んだノードに対して繰り返しリクエストを送ることはありません。

undici ProxyAgent、https-proxy-agent、socks-proxy-agentの比較

ツール 依存関係 SOCKS5のサポート HTTP/2 速度
undici ProxyAgent なし (Node 18+に組み込まれている) デフォルトではなし はい 高速
https-proxy-agent 1-2パッケージ なし なし 中程度
socks-proxy-agent 3-4パッケージ はい なし 中程度

最小限の依存関係で純粋なHTTP/HTTPS解析を行う場合、undici ProxyAgentは組み込みとHTTP/2との互換性のおかげで優れています。SOCKS5をプロダクションで使用する場合、手動のハンドシェイクを維持するよりも、信頼できるsocks-proxy-agentを使用する方が簡単です—上記のセクションの手動実装は学習や稀なエッジケースには良いですが、時間あたり何千もの接続を持つ負荷の高いプロダクションには適していません。

実用的なケース: 解析と地理テスト

マーケットプレイスでの価格監視。 undiciを使用したスクリプトは、プロキシのローテーションと上記のリトライロジックを持ち、WildberriesやOzonのレート制限を回避するのに最適です: 各次のリクエストの商品カードは新しいIPから送信され、ヘルスチェックは自動的に禁止されたアドレスをプールから除外します。

地理に基づく広告クリエイティブのテスト。 Google AdsやFacebook Adsの特定の国のユーザーにコンテンツがどのように提供されるかを確認する必要がある場合、ProxyAgentのプロキシURLを必要な地域のノードに変更するだけで済みます—APIやサーバーのHTMLレスポンスを介してチェックが行われる場合、ブラウザを立ち上げる必要はありません。

異なる国でのコンテンツの可用性の確認。 サイトがIPに応じて異なるコンテンツを提供するタスクでは、レジデンシャル接続はターゲットサーバーにとって通常のユーザーと区別がつかないため、データセンターの範囲と比較してブロックのリスクが低くなります。このシナリオは、ターゲットサービスがネットワークタイプを確認し、明示的なデータセンターのサブネットをブロックする場合、特に<а href="https://proxycove.com/ja/mobile-proxies/" style="color:#2563eb;">モバイルプロキシにとって重要です。

APIエンドポイントの大量チェック。 外部プロキシプールを介して独自のAPIの負荷テストを行う際は、接続プーリングに注意を払うことが重要です—undiciはデフォルトで1つのProxyAgent内でTCP接続を再利用するため、同じホストへの数百の連続リクエスト時のTLSハンドシェイクの時間を大幅に節約します。

結論

組み込みのundici ProxyAgentは、サードパーティのパッケージを持たずにNode.jsでのHTTP/HTTPSプロキシに関するほとんどのタスクをカバーします: URLを介した認証、CONNECTを介したトンネリング、HTTP/2との互換性、接続プーリングがすぐに機能します。プロキシのローテーションとヘルスチェックは、クリーンなプロミスとタイマーで簡単に構築でき、必要に応じてSOCKS5はカスタム接続コネクタを介して手動で実装されますが、プロダクションではSOCKS専用の既製ライブラリを使用する方が合理的です。

あなたのundiciスクリプトがマーケットプレイスの解析や価格監視を目的としている場合、レジデンシャルプロキシに注目してください—これらは従来のデータセンターIPよりもボット対策システムに対して耐性があり、Wildberries、Ozon、またはAvitoへの大量リクエスト時に追加のCAPTCHA解決を必要とすることが少なくなります。