ProxynetProxynet

استفاده از پروکسی در Node.js با Axios و node-fetch

تاریخ انتشار:

12 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
مکعب Node که به کارت‌های axios و fetch وصل است و آن‌ها به یک گره پروکسی می‌رسند

در پایتون، تنظیم پروکسی در بیشتر کتابخانه‌ها یک پارامتر proxies است. در Node.js کار آشفته‌تر است: fetch داخلی، Axios و node-fetch پروکسی را با سه سازوکار متفاوت می‌گیرند، SOCKS5 بسته جداگانه‌ای لازم دارد و وقتی گزینه نادرستی به کتابخانه بدهید اغلب حتی خطا هم نمی‌گیرید؛ درخواست بی‌صدا بدون پروکسی خارج می‌شود. به همین دلیل مهم است بدانید کدام کتابخانه کدام گزینه را می‌خواند.

در این نوشته سه روش پایه تنظیم پروکسی در Node.js (متغیر محیطی، agent و dispatcher)، استفاده از پروکسی دارای احراز هویت با fetch داخلی، Axios و node-fetch، پروکسی‌های SOCKS5 و چرخش IP همراه تلاش دوباره را با کد قابل اجرا بررسی می‌کنیم. نمونه‌ها را روی Node.js 24 در برابر پروکسی آزمون محلی که نام کاربری و رمز می‌خواست اجرا کردیم و ناسازگاری نسخه‌ای که با آن روبه‌رو شدیم و تفاوت نمایش خطاهای احراز هویت در کتابخانه‌ها را یادداشت کردیم.

راه‌های تنظیم پروکسی در Node.js

Node.js سه سازوکار برای فرستادن درخواست HTTP از طریق پروکسی دارد. کتابخانه‌ای که به کار می‌برید تعیین می‌کند کدام سازوکار اعمال شود.

  1. متغیر محیطی. متغیرهای HTTP_PROXY، HTTPS_PROXY و NO_PROXY. کل یک اسکریپت را بدون تغییر کد از پروکسی عبور می‌دهند، اما همه کتابخانه‌ها آن‌ها را نمی‌خوانند.
  2. Agent. ماژول‌های کلاسیک http و https در Node.js اتصال را از طریق شیء Agent باز می‌کنند. Axios و node-fetch از این ماژول‌ها استفاده می‌کنند، پس وقتی agent متصل به پروکسی (مانند HttpsProxyAgent) به آن‌ها بدهید، درخواست‌ها از پروکسی می‌گذرند.
  3. Dispatcher. تابع fetch داخلی Node.js از ماژول کلاسیک http استفاده نمی‌کند، بلکه کلاینتی به نام undici را به کار می‌برد. در undici شیئی که اتصال‌ها را مدیریت می‌کند dispatcher نام دارد و برای پروکسی به آن ProxyAgent می‌دهید.

این تمایز مهم‌ترین نکته نوشته است: fetch داخلی گزینه agent را نمی‌شناسد. اگر از روی عادت Axios بنویسید fetch(url, { agent })، هیچ خطایی نمی‌گیرید و درخواست بدون پروکسی خارج می‌شود.

سازوکار کلی پروکسی و تونل CONNECT که برای درخواست‌های HTTPS ساخته می‌شود را در سرور پروکسی چیست و چگونه کار می‌کند؟ توضیح داده‌ایم. برگرداندن فرمان‌های cURL به fetch و Axios (هدرها، بدنه، داده فرم) موضوع نوشته cURL در جاوااسکریپت است؛ این نوشته فقط بر سمت پروکسی تمرکز دارد.

چگونه با fetch داخلی از پروکسی استفاده کنیم؟

ProxyAgent در undici

بسته undici را نصب کنید:

bash
npm install undici

سپس fetch و ProxyAgent را از همان یک بسته وارد کنید:

javascript
import { fetch, ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://user:pass@pr.proxynet.io:8000");

const res = await fetch("https://httpbin.org/ip", {
  dispatcher,
  signal: AbortSignal.timeout(20_000),
});
console.log(res.status, await res.json());

با اینکه آدرس پروکسی با http:// آغاز می‌شود، با خیال راحت به سایت‌های HTTPS می‌رسید: ProxyAgent با پروکسی تونل CONNECT باز می‌کند و اتصال TLS با مقصد درون همین تونل برقرار می‌شود.

دام ناسازگاری نسخه. Node.js نسخه undici خودش را درون خود دارد؛ undici که از npm نصب می‌کنید ممکن است تازه‌تر باشد. وقتی ProxyAgent بسته npm را به fetch داخلی Node.js دادیم (یعنی fetch سراسری را بدون import به کار بردیم)، درخواست روی Node.js 24.11 همراه undici 8.10 با خطای fetch failed ناموفق شد و علت آن invalid onRequestStart method بود. به کار بردن همان agent با fetch خود undici بی‌مشکل کار کرد. قاعده ساده است: fetch را از همان بسته‌ای بردارید که ProxyAgent را برداشته‌اید.

برای اینکه همه فراخوانی‌های fetch از undici در برنامه از یک پروکسی استفاده کنند، می‌توانید dispatcher سراسری تعریف کنید:

javascript
import { fetch, ProxyAgent, setGlobalDispatcher } from "undici";

setGlobalDispatcher(new ProxyAgent("http://user:pass@pr.proxynet.io:8000"));

const res = await fetch("https://httpbin.org/ip"); // لازم نیست dispatcher بدهید

متغیر محیطی NODE_USE_ENV_PROXY

در نسخه‌های فعلی Node.js، وقتی NODE_USE_ENV_PROXY روشن باشد، fetch داخلی متغیرهای محیطی استاندارد پروکسی را می‌خواند. بر اساس راهنمای پیکربندی شبکه سازمانی در Node.js این قابلیت از نسخه‌های 22.21.0 و 24.5.0 در دسترس است و همین رفتار را می‌توان با گزینه خط فرمان --use-env-proxy هم روشن کرد.

در لینوکس و macOS:

bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" node app.mjs

در Windows PowerShell:

powershell
$env:NODE_USE_ENV_PROXY = "1"
$env:HTTPS_PROXY = "http://user:pass@pr.proxynet.io:8000"
node app.mjs

این روش به تغییر کد نیاز ندارد؛ fetch("https://httpbin.org/ip") مستقیم از پروکسی می‌گذرد. در آزمون ما، وقتی همان فرمان بدون NODE_USE_ENV_PROXY اجرا شد، با وجود تنظیم HTTPS_PROXY درخواست هرگز به پروکسی نرسید. آدرس‌های شبکه داخلی را می‌توانید با متغیر NO_PROXY از پروکسی بیرون نگه دارید.

اگر dispatcher می‌خواهید که متغیرهای محیطی را از درون کد بخواند، کلاس EnvHttpProxyAgent در undici همین کار را می‌کند:

javascript
import { fetch, EnvHttpProxyAgent } from "undici";

const res = await fetch("https://httpbin.org/ip", { dispatcher: new EnvHttpProxyAgent() });

چگونه با Axios از پروکسی استفاده کنیم؟

Axios در Node.js از ماژول‌های کلاسیک http و https استفاده می‌کند و دو راه برای به کار بردن پروکسی دارد.

گزینه proxy داخلی

گزینه proxy در پیکربندی درخواست در Axios برای آدرس‌های رمزنگاری‌نشده http:// کار می‌کند:

javascript
import axios from "axios";

const { data } = await axios.get("http://httpbin.org/ip", {
  proxy: {
    protocol: "http",
    host: "pr.proxynet.io",
    port: 8000,
    auth: { username: "user", password: "pass" },
  },
  timeout: 20_000,
});
console.log(data);

رمز درون فیلد auth کدگذاری نمی‌شود؛ نویسه‌های خاص را می‌توانید همان‌طور که هستند بنویسید.

https-proxy-agent برای آدرس‌های HTTPS

وقتی آدرس مقصد HTTPS است، سپردن ساخت تونل به یک agent نتیجه قابل‌اعتمادتری می‌دهد:

bash
npm install axios https-proxy-agent
javascript
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

const httpsAgent = new HttpsProxyAgent("http://user:pass@pr.proxynet.io:8000");

const client = axios.create({
  httpsAgent,
  proxy: false, // منطق پروکسی خود Axios را خاموش می‌کند و ساخت تونل را به agent می‌سپارد
  timeout: 20_000,
});

const { status, data } = await client.get("https://httpbin.org/ip");
console.log(status, data);

خط proxy: false را از قلم نیندازید. وقتی گزینه proxy داده نشود، Axios ممکن است بکوشد پروکسی متغیرهای محیطی را با منطق خودش اعمال کند و این یعنی دو رفتار ناسازگار پروکسی کنار agent. بستن agent یک بار با axios.create شما را از تکرار آن در هر فراخوانی بی‌نیاز می‌کند.

چگونه با node-fetch از پروکسی استفاده کنیم؟

پیش از آمدن fetch داخلی، node-fetch رایج‌ترین پیاده‌سازی fetch در Node.js بود و هنوز در پروژه‌های قدیمی زیاد دیده می‌شود. برخلاف fetch داخلی، از ماژول کلاسیک http استفاده می‌کند، پس پروکسی با گزینه agent داده می‌شود:

bash
npm install node-fetch https-proxy-agent
javascript
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";

const agent = new HttpsProxyAgent("http://user:pass@pr.proxynet.io:8000");

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

نسخه 3 از node-fetch فقط به‌صورت ماژول ES منتشر می‌شود؛ اگر در پروژه CommonJS که از require استفاده می‌کند نمی‌توانید آن را با import بارگذاری کنید، رفتن به fetch داخلی معمولاً کم‌زحمت‌تر است. دلیلی برای افزودن node-fetch به پروژه تازه وجود ندارد.

چگونه از پروکسی SOCKS5 استفاده کنیم؟

ProxyAgent در undici برای پروکسی‌های HTTP است و fetch داخلی از SOCKS5 پشتیبانی نمی‌کند. برای کار با پروکسی SOCKS5 بسته socks-proxy-agent را همراه Axios یا node-fetch به کار ببرید:

bash
npm install socks-proxy-agent
javascript
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";

// socks5h: نام دامنه در سمت پروکسی resolve می‌شود، پس هیچ پرس‌وجوی DNS از شبکه شما خارج نمی‌شود
const agent = new SocksProxyAgent("socks5h://user:pass@pr.proxynet.io:1080");

const { data } = await axios.get("https://httpbin.org/ip", {
  httpAgent: agent,
  httpsAgent: agent,
  proxy: false,
});
console.log(data);

در node-fetch همان agent به شکل fetch(url, { agent }) داده می‌شود. در لاگ‌های پروکسی آزمون ما، درخواست‌هایی که با طرح socks5h:// فرستاده شدند به‌صورت نام دامنه به پروکسی رسیدند، نه آدرس IP. طرح socks5:// نام دامنه را روی رایانه شما resolve می‌کند؛ مشکلاتی که این کار ایجاد می‌کند در نشت WebRTC و DNS آمده است. سازوکار SOCKS را در پروکسی SOCKS5: چطور کار می‌کند و چه فرقی دارد توضیح داده‌ایم.

کتابخانه‌ها پروکسی را چگونه می‌گیرند؟

کلاینتشیوه دادن پروکسیمتغیر محیطیSOCKS5خطای احراز هویت
fetch داخلیNODE_USE_ENV_PROXY یا --use-env-proxyفقط با پرچمنداردfetch failed
fetch در undicidispatcher: new ProxyAgent(...)با EnvHttpProxyAgentنداردfetch failed با علت لغو درخواست
Axiosگزینه proxy یا httpsAgent همراه proxy: falseاگر گزینه‌ای داده نشودبا socks-proxy-agentخطا با کد وضعیت 407
node-fetchagentنداردبا socks-proxy-agentبسته به agent
http.requestagentنداردبا socks-proxy-agentبسته به agent

ستون آخر هنگام اشکال‌زدایی مهم است. در آزمون ما با رمز نادرست، Axios علت را با پیام Request failed with status code 407 و error.response.status === 407 روشن نشان داد. تابع fetch در undici فقط خطای fetch failed داد و علت را «Request was cancelled» گزارش کرد؛ خود پیام از احراز هویت نامی نمی‌برد. اگر در undici fetch failed دیدید، نخستین چیزی که باید بررسی کنید نام کاربری و رمز پروکسی است. همه علت‌های 407 را در احراز هویت پروکسی: نام کاربری و رمز یا لیست سفید IP گرد آورده‌ایم.

اطلاعات ورود و نویسه‌های خاص

در همه روش‌هایی که آدرس پروکسی را به شکل URL می‌گیرند (ProxyAgent در undici، https-proxy-agent، socks-proxy-agent، متغیرهای محیطی)، نویسه‌هایی مانند @، :، / و # در رمز باید کدگذاری شوند. در غیر این صورت آدرس نادرست تفسیر می‌شود.

javascript
const user = process.env.PROXY_USER;
const pass = encodeURIComponent(process.env.PROXY_PASS);

const proxyUrl = `http://${user}:${pass}@pr.proxynet.io:8000`;

خواندن اطلاعات ورود از متغیرهای محیطی به‌جای نوشتن در کد، نمی‌گذارد رمز هر جا که کد به اشتراک گذاشته می‌شود دیده شود. در فیلد proxy.auth در Axios رمز بدون کدگذاری نوشته می‌شود.

چرخش IP و تلاش دوباره

در یک کار واقعی جمع‌آوری داده، درخواست‌ها گاهی ناموفق می‌شوند: نقطه خروج پروکسی به مقصد نمی‌رسد، مقصد 429 یا 503 برمی‌گرداند یا اتصال timeout می‌شود. نمونه زیر از میان چند پروکسی تصادفی انتخاب می‌کند، درخواست ناموفق را با backoff نمایی دوباره می‌فرستد و با دسته‌های کوچک تعداد درخواست‌های هم‌زمان را محدود می‌کند:

javascript
import { fetch, ProxyAgent } from "undici";

const PROXIES = [
  "http://user:pass@pr.proxynet.io:8000",
  "http://user:pass@pr.proxynet.io:8001",
];
const agents = PROXIES.map((p) => new ProxyAgent(p));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function getWithRetry(url, { attempts = 4, baseMs = 500 } = {}) {
  let lastError;
  for (let i = 0; i < attempts; i++) {
    const dispatcher = agents[Math.floor(Math.random() * agents.length)];
    try {
      const res = await fetch(url, { dispatcher, signal: AbortSignal.timeout(20_000) });
      if (res.status === 429 || res.status >= 500) {
        await res.body?.cancel();
        throw new Error(`HTTP ${res.status}`);
      }
      return await res.text();
    } catch (err) {
      lastError = err;
      await sleep(baseMs * 2 ** i + Math.random() * 250);
    }
  }
  throw lastError;
}

async function crawl(urls, concurrency = 5) {
  const results = [];
  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    results.push(...(await Promise.allSettled(batch.map((u) => getWithRetry(u)))));
  }
  return results;
}

const urls = ["https://httpbin.org/ip", "https://httpbin.org/status/503", "https://example.com/"];
const results = await crawl(urls, 2);
results.forEach((r, i) =>
  console.log(urls[i], r.status, r.status === "fulfilled" ? `${r.value.length} bytes` : r.reason.message),
);

سه جزئیات کد مهم است:

  • Promise.allSettled نمی‌گذارد درخواست ناموفق در یک دسته بقیه را متوقف کند. با Promise.all یک 503 نتیجه کل دسته را از بین می‌برد. در نمونه، آدرس /status/503 پس از چهار تلاش rejected شد و آدرس‌های دیگر نتیجه‌شان را برگرداندند.
  • res.body?.cancel() اتصال پاسخی را که قرار است دوباره تلاش شود بدون خواندن بدنه آزاد می‌کند. بدنه‌های خوانده‌نشده می‌توانند مخزن اتصال را پر کنند.
  • جزء تصادفی (Math.random() * 250) نمی‌گذارد درخواست‌هایی که هم‌زمان ناموفق شده‌اند هم‌زمان دوباره فرستاده شوند و انباشت تازه‌ای بسازند.

به‌جای مدیریت فهرست پروکسی به دست خودتان، اگر از پروکسی چرخشی استفاده کنید که از یک آدرس در هر اتصال IP خروجی متفاوتی می‌دهد، آرایه PROXIES یک عضو می‌شود و چرخش در سمت ارائه‌دهنده انجام می‌شود. برای کارهایی که باید در طول نشست همان IP حفظ شود (صفحه‌های نیازمند ورود، جریان‌های چندمرحله‌ای)، پروکسی با نشست ثابت ترجیح داده می‌شود.

این نمونه هدر Retry-After پاسخ 429 را نمی‌خواند. رویکردی که به هدر احترام می‌گذارد و کدهای وضعیتی را که نباید دوباره تلاش شوند جدا می‌کند در کدهای وضعیت HTTP در وب اسکرپینگ آمده است. اینکه مقدار همروندی در عمل چه اثری بر سرعت دارد در همروندی و موازی‌سازی بررسی شده است.

چگونه از کار کردن پروکسی مطمئن شویم؟

با هر پیکربندی تازه بررسی کنید که درخواست واقعاً از پروکسی می‌گذرد. ساده‌ترین راه، فرستادن درخواست به آدرسی است که IP خروجی را برمی‌گرداند؛ یک بار بدون پروکسی و یک بار با آن:

javascript
import { fetch, ProxyAgent } from "undici";

const ip = async (options = {}) => (await (await fetch("https://api.ipify.org?format=json", options)).json()).ip;

console.log("without proxy:", await ip());
console.log("with proxy:", await ip({ dispatcher: new ProxyAgent(process.env.HTTPS_PROXY) }));

اگر هر دو خط یک آدرس نشان دهند، درخواست از پروکسی نمی‌گذرد. رایج‌ترین علت، دادن agent به fetch داخلی یا به کار بردن agent در Axios بدون نوشتن proxy: false است.

اشتباهات رایج

  • دادن agent به fetch داخلی. گزینه بی‌صدا نادیده گرفته می‌شود. کلید درست در fetch، dispatcher است.
  • دادن ProxyAgent بسته npm از undici به fetch سراسری. وقتی نسخه‌ها جور نباشند خطای fetch failed می‌گیرید؛ fetch را هم از undici وارد کنید.
  • تنظیم HTTPS_PROXY و فرض خواندن آن توسط fetch داخلی. بدون NODE_USE_ENV_PROXY یا --use-env-proxy آن را نمی‌خواند.
  • ننوشتن proxy: false همراه agent در Axios. دو سازوکار پروکسی با هم تداخل می‌کنند.
  • کدگذاری نکردن نویسه‌های خاص رمز. آدرس نادرست تفسیر می‌شود و احراز هویت ناموفق است.
  • resolve محلی DNS با socks5://. برای resolve از راه دور socks5h:// را به کار ببرید.
  • تعیین نکردن timeout. نقطه خروج پروکسی که پاسخ نمی‌دهد، درخواست بدون timeout را دقیقه‌ها معطل می‌کند. در undici از AbortSignal.timeout و در Axios از timeout استفاده کنید.
  • آغاز همه درخواست‌ها یک‌جا با Promise.all. هم به مخزن اتصال خودتان و هم به محدودیت نرخ سایت مقصد فشار می‌آورد.

کدام روش را انتخاب کنیم؟

وضعیت شماپیشنهاد
پروژه تازه با وابستگی کمfetch در undici همراه ProxyAgent
عبور دادن اسکریپت موجود از پروکسی بدون تغییر کدNODE_USE_ENV_PROXY=1 همراه HTTPS_PROXY
پروژه از قبل Axios داردAxios همراه https-proxy-agent و proxy: false
پروژه قدیمی با node-fetchnode-fetch همراه agent
پروکسی SOCKS5Axios یا node-fetch همراه socks-proxy-agent و socks5h://
IP متفاوت در هر درخواستپروکسی چرخشی، یک آدرس
همان IP در طول نشستپروکسی sticky
پیام خطای روشن اهمیت داردAxios (کد 407 را با کد وضعیت نشان می‌دهد)

هیچ‌کدام از این‌ها درباره جاوااسکریپتی که در مرورگر اجرا می‌شود صدق نمی‌کند: fetch مرورگر نمی‌تواند پروکسی را از درون کد عوض کند؛ پروکسی در تنظیمات مرورگر یا سیستم‌عامل تعیین می‌شود. برای تنظیم در ویندوز، تنظیم پروکسی در ویندوز و کروم را ببینید.

پرسش‌های متداول

آیا fetch داخلی Node.js از پروکسی پشتیبانی می‌کند؟

از Node.js 22.21.0 و 24.5.0، وقتی متغیر محیطی NODE_USE_ENV_PROXY=1 یا گزینه --use-env-proxy به کار رود، متغیرهای HTTP_PROXY و HTTPS_PROXY را می‌خواند. برای تعیین پروکسی به تفکیک درخواست از درون کد، fetch و ProxyAgent از undici را به کار ببرید.

آیا Axios متغیر محیطی HTTPS_PROXY را می‌خواند؟

در Node.js وقتی گزینه proxy داده نشود، Axios می‌کوشد پروکسی متغیرهای محیطی را به کار ببرد. برای رفتاری روشن و قابل پیش‌بینی، پیشنهاد می‌کنیم پروکسی را با agent تعریف کنید و proxy: false بنویسید.

آیا می‌توانم برای هر درخواست پروکسی متفاوتی به کار ببرم؟

بله. در undici می‌توانید به هر فراخوانی fetch یک ProxyAgent متفاوت و در Axios یک httpsAgent متفاوت بدهید. به‌جای ساختن agent در هر درخواست، مانند نمونه بالا یک بار بسازید و دوباره به کار ببرید؛ هر agent تازه مخزن اتصال خودش را باز می‌کند.

هنگام استفاده از پروکسی، کوکی‌ها چگونه حفظ می‌شوند؟

fetch داخلی و undici کوکی نگه نمی‌دارند؛ باید هدر Set-Cookie را بخوانید و در درخواست بعدی به‌صورت هدر Cookie بیفزایید. در Axios می‌توانید از cookie jar مبتنی بر tough-cookie استفاده کنید. به یاد داشته باشید که همراه کوکی‌ها، آدرس IP هم باید در سراسر نشست ثابت بماند.

در Puppeteer یا Playwright پروکسی چگونه داده می‌شود؟

ابزارهای خودکارسازی مرورگر پروکسی را مانند کتابخانه‌های Node.js نمی‌گیرند؛ آن را به‌صورت گزینه هنگام اجرای مرورگر دریافت می‌کنند. agentهای این نوشته روی مرورگر اثری ندارند.

جاوااسکریپت را انتخاب کنم یا پایتون؟

پروکسی در هر دو زبان کار می‌کند؛ در پایتون بیشتر کتابخانه‌ها پروکسی را با یک پارامتر می‌گیرند، در حالی که در Node.js سازوکار با کتابخانه تغییر می‌کند. انتخاب معمولاً به زبان تیم و ساختار صفحه‌های مقصد بستگی دارد.

خلاصه

در Node.js پروکسی بسته به کلاینت به سه شکل داده می‌شود: dispatcher در undici یا NODE_USE_ENV_PROXY برای fetch داخلی، و agent برای Axios و node-fetch. برای مقصدهای HTTPS، Axios را با https-proxy-agent و proxy: false، و SOCKS5 را با socks-proxy-agent و طرح socks5h:// به کار ببرید. ProxyAgent و fetch را از یک بسته بردارید، رمز را کدگذاری کنید، timeout تعیین کنید و درخواست‌ها را در دسته‌های کوچک Promise.allSettled همراه منطق تلاش دوباره اجرا کنید. پلن‌های مناسب کار جمع‌آوری داده را در صفحه راه‌حل استخراج داده می‌یابید.

پرسش از ChatGPTپرسش از Claude