私たちは Geonode というプロキシ販売業者です。そこで、特定のクライアントで当社の製品を使用する際のガイドをご紹介します。他の部分は飛ばしても、この一文だけは必ず読んでください:設定後は必ず出口アドレスを確認してください。なぜなら、機能していないプロキシ設定でもエラーは一切発生しないからです。 SuperAgentは、リクエストを問題なく直接送信し、200を返し、プロキシがバイパスされたことを一切示しません。検証のセクションは4行のみですが、これが「知っている」ことと「推測している」ことの違いとなります。
なお、SuperAgentはブラウザでも動作しますが、その場合はここでの説明は一切当てはまりません。ブラウザではJavaScriptからプロキシの使用を指示できないため、ここでの内容はすべてNode専用です。
用語に関する問題
これは実際のミスを招く原因となるため、まずこの点を明確にしておきましょう。
SuperAgentにおいて、引数を指定せずに .agent() を実行すると、Cookie を保持する SuperAgent のコピーが作成されます。ドキュメントには次のように明記されています: 「Node.js では、SuperAgent はデフォルトで Cookie を保存しませんが、.agent() メソッドを使用することで、Cookie を保存する SuperAgent のコピーを作成できます。各コピーには個別の Cookie ジャーがあります。」
const agent = request.agent();
await agent.post("/login").send({ user, pass });
await agent.get("/cookied-page"); // session cookie carried over
そのエージェントにはデフォルト設定もあります:「エージェントに対して呼び出された通常のリクエストメソッドは、そのエージェントによるすべてのリクエストのデフォルトとして使用されます。」
一方、引数を指定して呼び出す .agent(httpAgent) は、リクエストに対して Node の http.Agent を設定します。ここがプロキシ機能の拠点となります。
同じメソッド名でありながら、2つの機能は互いに関連がなく、引数の有無だけで区別されます。SuperAgent エージェントとプロキシエージェントについて同じセッションで読み進めている場合は、何かを記述する前にこの点を明確にしておく価値があります。
ルート1:プロキシエージェント
推奨されるアプローチであり、その理由はメンテナンスのしやすさです。
import request from "superagent";
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://myuser:mypass@proxy.example.com:9000");
const res = await request
.get("https://api.example.com/items")
.agent(agent);
SOCKSの場合は、パッケージを以下のように変更してください:
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://proxy.example.com:1080");
socks5
ではなく、socks5h
である点に注意してください。h
バージョンでは、ホスト名の解決がローカルではなくプロキシ側で行われるため、トラフィックが別の場所を経由している間にも、DNS クエリが自身のリゾルバーに送信されるのを防ぎます。これは、地理的位置特定のためにプロキシを使用する意味を静かに無効にしてしまうリークです。
また、proxy-agent
は URL で指定されたプロトコルをすべて処理するため、プロキシが設定ファイルから読み込まれる場合に便利です:
import { ProxyAgent } from "proxy-agent";
const agent = new ProxyAgent(); // reads http_proxy / https_proxy / no_proxy
SuperAgent拡張機能ではなく、これらのパッケージを選ぶ理由: これら3つはいずれも活発にメンテナンスされています。2026年9月のnpmレジストリを確認したところ、proxy-agent
は8.0.2、https-proxy-agent
は9.1.0、socks-proxy-agent
は10.1.0となっており、いずれも2026年6月に公開されています。
方法その2:superagent-proxy
この目的のために特別に開発された拡張機能と、それに伴う注意点。
import request from "superagent";
import superagentProxy from "superagent-proxy";
superagentProxy(request);
const res = await request
.get("https://api.example.com/items")
.proxy("http://myuser:mypass@proxy.example.com:9000");
そのREADMEには、「superagentのRequestクラスを.proxy(uri)関数で拡張したもの」と説明されており、「proxy-agentモジュールを基盤としている」と記されています。
このAPIは、エージェントを直接渡す方法よりも使い勝手が良いです。.proxy(uri)という呼び出しは、処理チェーンの中で読みやすく、また「HTTP、HTTPS、またはSOCKS」のURIを受け付け、プロトコルの選択をproxy-agentに委ねています。
注意点はそのリリース日です。 superagent-proxy はバージョン3.0.0で、2021年9月に公開されました。この記事の執筆時点で約5年が経過していますが、その基盤となるproxy-agentの依存関係はその後もリリースを続けています。非推奨でもなく、動作しないわけでもありませんが、ラップ対象が進化している一方で、この薄いラッパー自体は更新されていない状態です。
実用上の影響としては、すでに使用していて問題なく動作している場合は、急いで対応する必要はありません。新しいコードを作成する場合、プロキシエージェントを直接使用すれば、1行追加するだけで済み、依存関係ツリーから古くなったレイヤーを取り除くことができます。また、この拡張機能はまさにそのエージェントをラップしたものなので、失うものは構文だけとなります。
実際に機能しているか確認する
最も重要な4行。
const res = await request
.get("https://api.ipify.org?format=json")
.agent(agent);
console.log(res.body);
エージェントを有効にした場合と無効にした場合で実行してみてください。アドレスが変わらない場合は、プロキシがパスに含まれていません。 この場合、SuperAgentからはエラーや警告は表示されず、リクエストの失敗も発生しません。トラフィックは単に直接送信されるだけです。
設定が機能しない一般的な理由は3つあります:
引数なしで .agent() を呼び出した ため、HTTPエージェントを設定する代わりに、クッキーが保持されるSuperAgentのコピーが作成されてしまいました。これは用語上の落とし穴であり、まさにこの症状を引き起こします。
エージェントを間違ったリクエストに適用してしまった。 SuperAgentのチェーン処理はリクエスト単位で行われるため、ある呼び出しで設定されたエージェントは次のリクエストには適用されません。一貫した動作を得るには、リクエストの作成を関数でラップしてください。
エージェントの種類がターゲットと一致していません。 HttpsProxyAgent は HTTPS ターゲットを処理しますが、通常の HTTP ターゲットには HTTP バリアントが必要になる場合があります。proxy-agent はこの問題を回避し、自動的に適切なエージェントを選択します。
地域ターゲティングを行うプロキシの場合、アドレスのチェックだけでは不十分です。 結果による検証 — 地域によって実際に異なる内容をリクエストし、レスポンスが変化したことを確認してください。ルックアップサービスが正しい国を報告しているにもかかわらず、APIが自社の地域データを返している場合、ターゲティングが本来意図した場所に到達していないことを意味します。これは、プロキシのテストが重要な理由で説明した「サイレント・フェイル」のパターンです。
プロキシ経由でのタイムアウト
SuperAgentのタイムアウトモデルは極めて優れており、プロキシによって遅延が生じる場合には、適切に活用する価値があります。
ドキュメントには2つの設定が記載されています。req.timeout({deadline: ms})
— または req.timeout(ms)
— は、「リクエスト全体(すべてのアップロード、リダイレクト、サーバーの処理時間を含む)が完了するまでの期限を設定します。 その時間内にレスポンスが完全にダウンロードされない場合、リクエストは中止されます。」また、req.timeout({response: ms})
は「サーバーからの最初のバイトが到着するまで待つ最大時間を設定しますが、ダウンロード全体にかかる時間を制限するものではありません。」
ドキュメントに記載されている設定に関するアドバイスは、プロキシ経由のリクエストに直接関連しています。「レスポンスタイムアウトは、サーバーが応答するのにかかる時間よりも少なくとも数秒長く設定する必要があります。これは、DNS 検索、TCP/IP および TLS 接続の確立、リクエストデータのアップロードにかかる時間も含まれるためです。」
プロキシを経由すると、これらの各フェーズにかかるコストが増加します。住宅用エグジットはリクエストごとの実質的な遅延をもたらしますが、これは障害ではなく距離によるものです。
const res = await request
.get("https://api.example.com/items")
.agent(agent)
.timeout({ response: 15000, deadline: 60000 });
ドキュメントでは両方の設定を併用することを推奨しており、その理由は他のあらゆる場面と同じです。レスポンスタイムアウトは応答を全く返さないサーバーを検知し、デッドラインは応答はするもののデータが少しずつしか届かないサーバーを検知します。 どちらか一方だけでは、両方のケースをカバーできません。
値の設定は、習慣ではなく、プロキシ経由での測定結果に基づいて行ってください。そうしないと、プロキシの不具合のように見えるが、実際には単に想定外の遅延による失敗を引き起こすことになります。
エラー処理
SuperAgentのデフォルトの挙動は、ほとんどのクライアントとは異なり、この点において重要な意味を持ちます。
ドキュメントには次のように明記されています。「SuperAgentは、デフォルトで4xxおよび5xxのレスポンス(ならびに未処理の3xxレスポンス)をエラーとみなします」。 さらに、「このステータス情報は err.status 経由で取得可能」であり、そのようなエラーには「err.response フィールドも含まれる」と付け加えられています。
したがって、プロキシ認証の失敗は、レスポンスではなくリジェクションとして届きます:
try {
const res = await request.get(url).agent(agent).timeout({ deadline: 30000 });
return res.body;
} catch (err) {
if (err.status === 407) throw new Error("Proxy rejected credentials");
if (err.status === 401) throw new Error("Target requires authentication");
if (!err.status) throw new Error(`Network error: ${err.code} ${err.message}`);
throw err;
}
407 と 401 の違いは、実装に組み込む価値のあるものです。 407はプロキシによってアクセスが阻止されたことを意味し、ターゲットには到達していません。一方、401はプロキシが正常に機能し、ターゲット側が認証情報を要求していることを意味します。原因も対処法も異なるため、両方がスローされたエラーとして表示されると、極めて混同されやすくなります。
err.statusが含まれていないエラーは、HTTPレスポンスがまったく届いていないことを意味し、これは認証の問題ではなく接続の問題を示しています。ECONNREFUSEDは、プロキシのアドレスでリスニングしているものが何もないことを意味し、ETIMEDOUTは、パケットが消失していることを意味します。
一部のエラーステータスを成功として扱う場合(404を失敗ではなくデータとして読み取るなど)、.ok()を使用してください:
.ok(res => res.status < 500)
再試行について(注意が必要)
SuperAgent には再試行機能が組み込まれていますが、ドキュメントに記載されている制限事項を遵守する必要があります。
const res = await request.get(url).agent(agent).retry(2);
ドキュメントによると、.retry()は、「一時的なエラーや不安定なインターネット接続が原因と思われる失敗が発生した場合、リクエストを自動的に再試行する」と説明されており、オプションの再試行回数(デフォルトは1)と、「各再試行の前に」呼び出されるコールバックを受け取ります。 このコールバックは、「true/false を返すことで、リクエストを再試行するかどうかを制御できます(ただし、最大再試行回数は常に適用されます)」。
そして、ドキュメントに明記されている制限事項として、.retry() は「冪等なリクエストでのみ」使用してください。
プロキシを経由する場合、特定の理由から、この点は通常以上に重要になります。タイムアウトは失敗の証拠ではありません。リクエストは宛先に到達して成功していたものの、応答が戻ってくる途中で失われた可能性もあります。そのような事態が起こり得る追加のホップが存在するからです。 そのような状況でPOSTリクエストを再試行すると、書き込みが重複する恐れがあり、いかなる再試行設定を行ってもこれを安全にすることはできません。操作が重要な場合は、APIが提供しているなら、冪等性キーを使用してください。
また、認証失敗時の再試行を回避するには、コールバックが適切な場所です。認証情報が間違っている場合の407エラーは、何度試行しても407エラーが返されるためです:
.retry(3, (err, res) => {
if (res?.status === 407 || res?.status === 401) return false;
return true;
})
作成する価値のあるラッパー
SuperAgentのチェイニングはリクエスト単位で行われるため、肝心な1つの呼び出しにおいてプロキシ設定を忘れてしまいがちです。リクエストの作成をラッパーで包むことでこの問題を解決し、残りのデフォルト設定を格納する場所を確保できます。
import request from "superagent";
import { ProxyAgent } from "proxy-agent";
const agent = process.env.PROXY_URL ? new ProxyAgent(process.env.PROXY_URL) : undefined;
const UA = "AcmeBot/1.0 (+https://acme.example.com/bot)";
function req(method, url) {
const r = request[method](url)
.set("User-Agent", UA)
.timeout({ response: 15000, deadline: 60000 })
.retry(2, (err, res) => {
if (res?.status === 407 || res?.status === 401) return false;
if (res?.status === 429) return false; // honour the rate limit instead
return true;
});
return agent ? r.agent(agent) : r;
}
export const get = url => req("get", url);
export const post = url => req("post", url);
export async function verifyExit() {
const res = await get("https://api.ipify.org?format=json");
console.log(`Exit address: ${res.body.ip}`);
return res.body.ip;
}
ここにある5つの選択肢は、意図的に設けられたものです。
プロキシはオプションであり、環境変数から取得されます。 PROXY_URL を指定しない場合、エージェントは undefined となり、リクエストは直接送信されます。これにより、アプリケーションコードに分岐を設けることなく、ローカル開発と本番環境での動作を予測可能に保てます。ソースコードには認証情報が一切表示されません。
コンストラクタ引数なしで ProxyAgent を指定すると、環境主導の設定を好む場合、http_proxy などの設定が読み込まれます。 URL を明示的に渡すことで、信頼できる情報源が明確になります。通常、この方が価値があります。
ユーザーエージェントは正直であり、連絡先 URL を含んでいます。 これには何のコストもかからず、サイト運営者があなたに気づいた際の対応が変わります。
再試行では、再試行が無意味または失礼となるステータスは除外されます。 認証情報が不正な場合の 407 は、いつ試行しても 407 のままです。429 は処理を遅らせるよう指示するものであり、これに再試行を行うと、一時的な制限が長期的な制限へと変わってしまいます。
verifyExit()はエクスポートされ、起動時に呼び出されます。 わずか6行のコードで、黙ってバイパスされていたプロキシをログに記録されるものに変えます。これは、あらゆるクライアントにおけるプロキシ処理の唯一の共通テーマであり、どのライブラリも代わりにやってくれない唯一の作業です。
「.connect()」はプロキシではない
一見プロキシのように見えるが、実際にはそうではないため、注意すべき点である。
SuperAgentには「.connect()」というメソッドがあり、ドキュメントによると、これにより「DNS解決を無視し、すべてのリクエストを特定のIPアドレスに転送する」ことが可能になる。このメソッドはマッピングをサポートしており、*へのフォールバックも含まれている:
const res = await request.get("http://redir.example.com:555")
.connect({
"redir.example.com": "127.0.0.1",
"www.example.com": false,
"mapped.example.com": { host: "127.0.0.1", port: 8080 },
"*": "proxy.example.com",
});
ドキュメントには、「リクエストはHostヘッダーを元の値のまま保持する」と記載されており、.connect(undefined)を指定するとこの機能を無効にできるとされています。
これはホストリダイレクトであり、プロキシではありません。リクエストそのものは変更せずに、接続先のアドレスを変更するだけです。CONNECTトンネルも、プロキシプロトコルも、プロキシ認証も存在しません。 これはテスト目的で存在しており、ドキュメントで「localhost でのテスト」の項目にまとめられているのにはそれなりの理由があります。
公式の例にある "*": "proxy.example.com" という行が、混乱の原因となっています。リクエストをローカルのテストサーバーに向けるには .connect() を使用し、実際のプロキシにはエージェントを使用してください。
よくある質問
SuperAgentでプロキシを使用するにはどうすればよいですか?
.agent()にプロキシエージェントを渡します。プロキシURLを指定してHttpsProxyAgentまたはSocksProxyAgentを作成し、それをリクエストに渡します。あるいは、superagent-proxy拡張機能を使用することもできます。これにより.proxy(uri)メソッドが追加されますが、このパッケージは2021年以来リリースされていません。
引数ありと引数なしの .agent() の違いは何ですか?
引数がない場合は、独自の JAR ファイルとデフォルトのオプションを持つ、Cookie を保持する SuperAgent のインスタンスを作成します。引数がある場合は、そのリクエストに対して Node.js の http.Agent を設定します。これがプロキシ機能の適用方法です。名前が同じであるため、実際に混乱を招くことがあります。
superagent-proxy は現在もメンテナンスされていますか?
非推奨にはなっていませんが、バージョン3.0.0は2021年9月のリリースであり、その基盤となるproxy-agentの依存関係はその後もリリースが続いており、直近では2026年6月にリリースされています。新しいコードでは、プロキシエージェントを直接使用することで、1行分のコードが増えるだけで、古くなったラッパーの使用を回避できます。
SuperAgentのプロキシが動作しないのはなぜですか?
ほとんどの場合、.agent()が引数なしで呼び出されたためです。これにより、プロキシが設定されるのではなく、クッキーの保存先が作成されてしまいます。また、SuperAgentのチェーン処理は呼び出しごとに適用されるため、エージェントが正しいリクエストに適用されているか確認してください。自分のアドレスを返すサービスにリクエストを送信して確認してください。プロキシがバイパスされている場合、エラーは発生しません。
SuperAgentはHTTP_PROXY環境変数を尊重しますか?
それ自体では尊重しません。proxy-agentパッケージはhttp_proxy、https_proxy、no_proxyを読み取ります。そのため、引数なしでProxyAgent()を構築し、それを.agent()に渡すことで、環境変数に基づいた動作を実現できます。
プロキシ経由のリクエストのタイムアウトを設定するにはどうすればよいですか?
次の2つの設定を両方とも使用してください:.timeout({ response: 15000, deadline: 60000 })。レスポンスタイムアウトは最初のバイトの受信待ち時間を制限し、デッドラインはリクエスト全体を制限します。住宅用回線の出口サーバーは、DNS、接続、TLSの各フェーズに実際の遅延を加えるため、プロキシ経由で測定した値に基づいてこれらの値を調整してください。
プロキシエラーとターゲットエラーをどのように見分ければよいですか?
ステータスコードで判別します。SuperAgentは4xxおよび5xxをエラーとして扱います。そのため、err.status をキャッチして読み取ってください。407はプロキシがリクエストを拒否し、ターゲットに到達しなかったことを意味し、401はプロキシは正常に動作したが、ターゲット側が認証情報を要求していることを意味します。 err.statusが全く返ってこない場合は、HTTPレスポンスが到着しなかったことを意味します。
.connect()をプロキシとして使用できますか?
いいえ。これは、元のHostヘッダーを維持したままリクエストを特定のIPにリダイレクトするものであり、プロキシ機能ではなくテスト用のホストマッピングです。トンネルも、プロキシプロトコルも、認証もありません。本格的なプロキシ機能を利用するには、エージェントを使用してください。
まとめ
SuperAgent自体にはプロキシ設定のオプションがないため、エージェントか拡張機能のどちらかを選択することになります。どちらの場合も、実際の処理を行うのはメンテナンスされているパッケージであるため、デフォルトではエージェントの方が適しています。
主な落とし穴は用語の使い方です。引数を指定せずに .agent() を実行すると、クッキーの保存先が設定されます。一方、.agent(something) を実行すると、HTTP エージェントが設定されます。多くの人は前者を設定し、リクエストが成功するのを見て、「プロキシが機能している」と結論づけてしまいます。しかし、バイパスされたプロキシは定義上、エラーを返さずに失敗するため、この誤解を正すものは何もありません。
だからこそ、検証を習慣化することが重要です。エージェントを設定した場合と設定しない場合の両方で、自分のアドレスを返すサービスにリクエストを送り、応答が変化することを確認してください。地域ターゲティングを行う場合は、さらに一歩進んで、地域ごとに異なるコンテンツが実際に異なっていることを確認しましょう。アドレスの確認は簡単な部分ですが、そこから得られる情報は最も少ないのです。
次に、両方のタイムアウトを設定し、err.status に基づいて分岐させ、407 と 401 が異なるメッセージにつながるようにします。また、.retry() は、冪等性を持たないものには使用しないようにしてください。プロキシを経由すると、成功したリクエストのレスポンスが失われる可能性のある余分なホップが生じます。そのような状況での再試行は、復旧ではなく重複となります。