DenoとBunでのプロキシ利用:開発者向け実践ガイド

DenoとBunのネイティブfetchはデフォルトでプロキシを無視します。本記事ではDeno.createHttpClientとBunのfetch proxyオプションを使い、geoターゲティング、スティッキーセッション、SOCKS5、リトライ戦略までを解説します。

Using Proxies in Deno and Bun: A Code-First Guide for Modern JavaScript

モダンなJavaScriptランタイムであるDenoとBunでスクレイピングや自動化を行う際、多くの開発者が最初に直面する壁が「DenoとBunでのプロキシ利用」です。標準のfetch()は環境変数すら暗黙に読み込まないため、そのままではプロキシ経由でリクエストを送れません。本記事では、DenoのDeno.createHttpClientとBunのfetch(url, { proxy })を使った実践的なプロキシ設定方法を、実行可能なコードとともに解説します。

DenoとBunでのプロキシ利用:なぜfetchはプロキシを無視するのか

Node.jsのnode-fetchaxiosとは異なり、DenoとBunのネイティブfetch()はWHATWG Fetch仕様に忠実に従って実装されています。WHATWG Fetch標準にはプロキシ設定の概念が含まれておらず、ブラウザのfetchと同様にプロキシを明示的に渡す必要があります。これは仕様上の正しい振る舞いであり、バグではありません(WHATWG Fetch Standardを参照)。

この設計には合理的な理由があります。ブラウザ環境ではプロキシはOSやブラウザ設定で管理され、JavaScript APIからは触れません。DenoとBunはこのモデルをサーバーサイドに持ち込んだため、開発者はプロキシ設定を明示的にコードで表現する必要があります。

各ランタイムのアプローチ比較

特徴DenoBun
プロキシ設定方法Deno.createHttpClient({ proxy })fetchclientオプションに渡すfetch(url, { proxy: 'http://...' })の1行で指定
HTTP_PROXY環境変数Deno 1.41+でcreateHttpClient未指定時に自動読み込みBun 1.1+で自動読み込み対応
SOCKS5サポートネイティブでサポート(socks5://ネイティブでサポート
カスタムCA証明書caCertsオプションで明示指定可能環境変数NODE_EXTRA_CA_CERTSで対応
APIの簡潔さクライアントオブジェクトを再利用可能リクエストごとに1行で完結

Denoでのプロキシ設定:Deno.createHttpClientを使う

Denoでは、Deno.createHttpClient()でプロキシ対応のHTTPクライアントを作成し、それをfetch()の第2引数{ client }に渡します。このクライアントオブジェクトは再利用可能で、接続プールやTLS設定を一元管理できます。

// deno run --allow-net --unstable-net proxy_basic.ts

// ProxyHatのHTTPプロキシを使用する基本的な例
const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: {
      username: "user-country-US",
      password: "pass",
    },
  },
});

try {
  const res = await fetch("https://httpbin.org/ip", { client });
  const body = await res.json();
  console.log("出口IP:", body.origin);
} catch (err) {
  console.error("リクエスト失敗:", err);
} finally {
  client.close(); // クライアントを閉じて接続を解放
}

ポイントはbasicAuthでユーザー名にgeoターゲティングフラグを埋め込むことです。user-country-USとすれば米国のIPが割り当てられます。Deno公式ドキュメントで全オプションを確認できます。

SOCKS5プロキシをDenoで使う

// SOCKS5を使用する場合、ポート1080を指定
const socksClient = Deno.createHttpClient({
  proxy: {
    url: "socks5://gate.proxyhat.com:1080",
    basicAuth: {
      username: "user-country-DE-city-berlin",
      password: "pass",
    },
  },
});

const res = await fetch("https://httpbin.org/ip", { client: socksClient });
console.log(await res.json());
socksClient.close();

Bunでのプロキシ設定:fetchのproxyオプション

Bunのアプローチはさらにシンプルです。fetch()の第2引数にproxyキーを渡すだけで、プロキシURLにユーザー名とパスワードを含めて認証できます。

// Bun: proxyオプションを1行で指定
const res = await fetch("https://httpbin.org/ip", {
  proxy: "http://user-country-US:pass@gate.proxyhat.com:8080",
});

const data = await res.json();
console.log("出口IP:", data.origin);

Bunではクライアントオブジェクトを明示的に作成する必要がなく、リクエストごとにプロキシを切り替えるのが容易です。ただし、多数のリクエストで同じプロキシを使う場合は、URL文字列を変数に保持して再利用することを推奨します。

BunでSOCKS5とgeoターゲティング

// Bun: SOCKS5 + 都市レベルのgeoターゲティング
const proxyUrl = "socks5://user-country-DE-city-berlin:pass@gate.proxyhat.com:1080";

const res = await fetch("https://httpbin.org/ip", {
  proxy: proxyUrl,
});
console.log(await res.json());

環境変数によるプロキシ設定:HTTP_PROXYとHTTPS_PROXY

CLIツールやスクリプトで一時的にプロキシを通したい場合は、環境変数HTTP_PROXYおよびHTTPS_PROXYが便利です。Deno 1.41以降およびBun 1.1以降では、createHttpClientproxyオプションを指定しない場合にこれらの環境変数を自動的に読み込みます。

# 環境変数を設定してからスクリプトを実行
export HTTP_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"
export HTTPS_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"

# Denoの場合
 deno run --allow-net --allow-env script.ts

# Bunの場合
bun run script.ts

ただし、本番環境ではクライアントごとの明示的設定を推奨します。理由は以下の通りです:

  • 環境変数は他のプロセスやライブラリにも影響を与える
  • リクエストごとに異なるgeoやセッションを指定できない
  • デバッグ時にプロキシの有無が不明瞭になる

プロトタイピングやワンショットのスクリプトでは環境変数が手軽ですが、並行リクエストでセッションを分散させる本番コードではコード内でプロキシを制御すべきです。

高ブロック率ターゲット向け:レジデンシャルプロキシとスティッキーセッション

データセンタープロキシは高速ですが、多くのターゲットサイト(ECサイト、SERP、ソーシャルメディア)がデータセンターIPレンジをブロックします。レジデンシャルプロキシは実際のISPに割り当てられたIPを使用するため、ブロックされる確率が大幅に下がります。

ここでは、複数のスティッキーセッションを並行実行し、AbortControllerでタイムアウトを管理する実践的な例を示します。スティッキーセッションを使うと、同一セッションIDの間は同じIPが維持され、ログイン後の状態保持やページネーションで必要になります。

// Deno / Bun 両方で動作する並行スクレイピング例
// 各セッションに異なるgeoとセッションIDを割り当て

const PROXY_BASE = "http://gate.proxyhat.com:8080";
const PROXY_USER = "user";
const PROXY_PASS = "pass";

interface FetchTask {
  url: string;
  country: string;
  sessionId: string;
}

async function fetchWithProxy(task: FetchTask, timeoutMs = 10000): Promise<unknown> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  const proxyUrl = `${PROXY_BASE}`;
  const username = `${PROXY_USER}-country-${task.country}-session-${task.sessionId}`;

  try {
    // Denoの場合
    const client = Deno.createHttpClient({
      proxy: {
        url: proxyUrl,
        basicAuth: { username, password: PROXY_PASS },
      },
    });

    const res = await fetch(task.url, {
      client,
      signal: controller.signal,
    });
    client.close();

    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    if (err.name === "AbortError") {
      console.error(`タイムアウト: ${task.url}`);
    } else {
      console.error(`エラー: ${task.url}`, err);
    }
    return null;
  } finally {
    clearTimeout(timer);
  }
}

// 5つのセッションを並行実行
const tasks: FetchTask[] = [
  { url: "https://httpbin.org/ip", country: "US", sessionId: "s001" },
  { url: "https://httpbin.org/ip", country: "DE", sessionId: "s002" },
  { url: "https://httpbin.org/ip", country: "JP", sessionId: "s003" },
  { url: "https://httpbin.org/ip", country: "GB", sessionId: "s004" },
  { url: "https://httpbin.org/ip", country: "FR", sessionId: "s005" },
];

const results = await Promise.all(tasks.map((t) => fetchWithProxy(t)));
console.log("結果:", results.filter(Boolean).length, "/", tasks.length, "成功");

このパターンの重要な要素は以下の通りです:

  • セッションIDsession-abc123で同じIPを維持。ページ遷移やログイン状態の保持に必須。
  • AbortController:10秒でタイムアウト。ハングした接続を放置しない。
  • Promise.all:5セッションを同時実行し、スループットを向上。
  • エラーハンドリングAbortErrorとその他を区別してログに記録。

Bunでの並行プロキシリクエスト

// Bun版:proxyオプションをリクエストごとに指定
const PROXY_USER = "user";
const PROXY_PASS = "pass";

async function fetchWithBunProxy(
  url: string,
  country: string,
  sessionId: string,
  timeoutMs = 10000
): Promise<unknown> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  const proxyStr = `http://${PROXY_USER}-country-${country}-session-${sessionId}:${PROXY_PASS}@gate.proxyhat.com:8080`;

  try {
    const res = await fetch(url, {
      proxy: proxyStr,
      signal: controller.signal,
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error(`エラー (${country}/${sessionId}):`, err.message);
    return null;
  } finally {
    clearTimeout(timer);
  }
}

const countries = ["US", "DE", "JP", "GB", "FR"];
const results = await Promise.all(
  countries.map((c, i) =>
    fetchWithBunProxy("https://httpbin.org/ip", c, `bun-s${i}`)
  )
);
console.log(results);

本番運用のベストプラクティス

リトライとバックオフ

プロキシ経由のリクエストは、IPローテーションや一時的なブロックにより失敗することがあります。指数バックオフによるリトライを実装することで、成功率を大幅に向上できます。

// リトライ付きプロキシfetch(Deno用)

async function fetchWithRetry(
  url: string,
  client: Deno.HttpClient,
  maxRetries = 3,
  baseDelay = 500
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, { client });
      if (res.ok) return res;
      if (res.status === 429 || res.status >= 500) {
        throw new Error(`リトライ対象: HTTP ${res.status}`);
      }
      return res; // 4xxはリトライしない
    } catch (err) {
      if (attempt === maxRetries) throw err;
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 200;
      console.log(`リトライ ${attempt + 1}/${maxRetries}、${Math.round(delay)}ms待機`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error("到達不能");
}

// 使用例
const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: { username: "user-country-US", password: "pass" },
  },
});

const res = await fetchWithRetry("https://httpbin.org/get", client);
console.log(await res.json());
client.close();

カスタムCA証明書(Deno)

企業プロキシ環境や自己署名証明書を使う場合、DenoのcaCertsオプションでルート証明書を追加できます。

// カスタムCA証明書を追加
const caCert = await Deno.readTextFile("./custom-ca.pem");

const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: { username: "user-country-US", password: "pass" },
  },
  caCerts: [caCert], // 追加のCA証明書
});

const res = await fetch("https://httpbin.org/get", { client });
client.close();

接続の再利用

DenoのHttpClientは内部で接続プールを管理します。複数リクエストで同じクライアントを使い回すことで、TLSハンドシェイクのオーバーヘッドを削減できます。100リクエストを送る場合、毎回createHttpClientを呼ぶのではなく、1つのクライアントを使い続けることでレイテンシを約200ms短縮できるケースがあります。

ProxyHat Node SDKとのサイドバイサイド

ProxyHatはNode.js SDKを提供しており、DenoとBunの両方で互換性モードで動作します(BunはNode API互換、Denoはnpm:指定で利用可能)。SDKを使うと、プロキシプール管理やローテーション、エラーハンドリングを抽象化できます。

// ProxyHat SDK(Deno: npm:指定、Bun: そのままimport)
// Denoの場合: import { ProxyHat } from "npm:@proxyhat/sdk";
// Bunの場合:  import { ProxyHat } from "@proxyhat/sdk";

import { ProxyHat } from "npm:@proxyhat/sdk";

const ph = new ProxyHat({
  username: "user",
  password: "pass",
  gateway: "gate.proxyhat.com",
  port: 8080,
});

// SDKがプロキシURLを構築・管理
const proxyUrl = ph.getProxyUrl({
  country: "US",
  session: "sdk-session-001",
});

console.log("生成されたプロキシURL:", proxyUrl);
// => http://user-country-US-session-sdk-session-001:pass@gate.proxyhat.com:8080

// これをDenoまたはBunのfetchに渡す
const res = await fetch("https://httpbin.org/ip", {
  proxy: proxyUrl, // Bunの場合
});
console.log(await res.json());

SDKを使う利点は、ユーザー名エンコーディングのミスを防げる点と、セッション管理を一元化できる点です。ただし、DenoのcreateHttpClientを使いたい場合は、SDKが生成したURLをproxy.urlに渡し、basicAuthは空にしてURL内認証に任せる形になります。詳細はProxyHat公式ドキュメントを参照してください。

倫理的スクレイピングと法的考慮事項

プロキシを使うからといって、スクレイピングの倫理的・法的制約が免除されるわけではありません。以下の原則を守るべきです:

  • 公開データのみを収集する:ログイン背後のデータやAPIキーで保護されたエンドポイントは避ける。
  • 公式APIを優先する:多くのプラットフォームが公式APIを提供しており、スクレイピングより安定。
  • robots.txtを尊重する:サイト管理者の意思を無視しない。
  • 米国CFAA:Computer Fraud and Abuse Actは不正アクセスを禁止し、ToS違反がCFAA違反に問われるケースがある(Van Buren v. United States (2021)の判決が参照基準)。
  • EU GDPR:個人データの収集には適法な根拠が必要。公開ウェブページでも個人データに該当する場合は同意または正当な利益が必要。
  • レート制限を守る:1秒間に100リクエストのような過負荷は避け、対象サーバーに負荷をかけない。

プロキシは技術的なツールであり、使い方の責任は開発者にあります。WebスクレイピングのユースケースSERPトラッキングのページでも、実践的なガイドラインを確認できます。

Key Takeaways

  • DenoはDeno.createHttpClient({ proxy })でクライアントを作成し、fetch(url, { client })に渡す。クライアントは再利用可能。
  • Bunはfetch(url, { proxy: 'http://...' })の1行で完結。リクエストごとの切り替えが容易。
  • ユーザー名にcountry-US-session-abc123を埋め込むことでgeoターゲティングとスティッキーセッションを同時に実現。
  • SOCKS5はポート1080を使用:socks5://user:pass@gate.proxyhat.com:1080
  • AbortControllerでタイムアウトを管理し、指数バックオフでリトライ。本番では必須。
  • レジデンシャルプロキシはデータセンターブロックを回避する鍵。ProxyHatの料金でプランを確認。
  • 公開データのみを収集し、公式APIを優先。CFAAとGDPRを遵守。

ProxyHatは100以上の国と地域に対応するレジデンシャル、モバイル、データセンタープロキシを提供しています。DenoとBunでのプロキシ利用において、gate.proxyhat.com:8080(HTTP)または:1080(SOCKS5)を通じて、安定したスクレイピング基盤を構築できます。まずはプランを確認し、上記のコード例をそのまま実行してテストしてみてください。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る