در پایتون، تنظیم پروکسی در بیشتر کتابخانهها یک پارامتر 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 از طریق پروکسی دارد. کتابخانهای که به کار میبرید تعیین میکند کدام سازوکار اعمال شود.
- متغیر محیطی. متغیرهای
HTTP_PROXY،HTTPS_PROXYوNO_PROXY. کل یک اسکریپت را بدون تغییر کد از پروکسی عبور میدهند، اما همه کتابخانهها آنها را نمیخوانند. - Agent. ماژولهای کلاسیک
httpوhttpsدر Node.js اتصال را از طریق شیءAgentباز میکنند. Axios و node-fetch از این ماژولها استفاده میکنند، پس وقتی agent متصل به پروکسی (مانندHttpsProxyAgent) به آنها بدهید، درخواستها از پروکسی میگذرند. - 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 را نصب کنید:
npm install undiciسپس fetch و ProxyAgent را از همان یک بسته وارد کنید:
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 سراسری تعریف کنید:
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:
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" node app.mjsدر Windows 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 همین کار را میکند:
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:// کار میکند:
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 نتیجه قابلاعتمادتری میدهد:
npm install axios https-proxy-agentimport 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 داده میشود:
npm install node-fetch https-proxy-agentimport 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 به کار ببرید:
npm install socks-proxy-agentimport 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 در undici | dispatcher: new ProxyAgent(...) | با EnvHttpProxyAgent | ندارد | fetch failed با علت لغو درخواست |
| Axios | گزینه proxy یا httpsAgent همراه proxy: false | اگر گزینهای داده نشود | با socks-proxy-agent | خطا با کد وضعیت 407 |
| node-fetch | agent | ندارد | با socks-proxy-agent | بسته به agent |
http.request | agent | ندارد | با 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، متغیرهای محیطی)، نویسههایی مانند @، :، / و # در رمز باید کدگذاری شوند. در غیر این صورت آدرس نادرست تفسیر میشود.
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 نمایی دوباره میفرستد و با دستههای کوچک تعداد درخواستهای همزمان را محدود میکند:
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 خروجی را برمیگرداند؛ یک بار بدون پروکسی و یک بار با آن:
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-fetch | node-fetch همراه agent |
| پروکسی SOCKS5 | Axios یا 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 همراه منطق تلاش دوباره اجرا کنید. پلنهای مناسب کار جمعآوری داده را در صفحه راهحل استخراج داده مییابید.




