خطای ⁦TypeError: fetch failed⁩ در Node.js: علت و راه رفع

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

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

Acar Diveroli
نویسنده: Acar Diveroli
سطر ⁦await fetch(url)⁩ با مکان‌نمای آبی؛ مسیر پس از TLS در RESET قرمز قطع می‌شود و خروجی ⁦cause: ECONNRESET⁩ را نشان می‌دهد
فهرست مطالب

اسکریپت Node.js شما هفته‌هاست هر ده دقیقه یک بار همان API را فراخوانی می‌کند. آن را به یک سرور CI منتقل می‌کنید، یا همکارتان آن را در شبکه دفتر اجرا می‌کند، و حالا هر فراخوانی با یک سطر تمام می‌شود: TypeError: fetch failed. نه نام میزبانی در کار است، نه کد وضعیتی. آدرس در مرورگر باز می‌شود و curl هم از همان ماشین به آن می‌رسد، پس پیام انگار هیچ چیزی نمی‌گوید.

اما چیزی می‌گوید، فقط نه در خود پیام. این راهنما نشان می‌دهد Node.js علت واقعی را کجا نگه می‌دارد، علت‌هایی که روی ⁦Node.js 22⁩، 24 و 26 بازتولید کردیم کدام‌اند، چرا fetch متغیرهای پروکسی را نادیده می‌گیرد، کدام ناسازگاری undici روی ⁦Node.js 26⁩ agentهای پروکسی را از کار می‌اندازد، و اسکریپتی آزموده‌شده را معرفی می‌کند که علت را نام می‌برد.

خطای ⁦TypeError: fetch failed⁩ یعنی چه؟

تابع سراسری fetch() در Node.js، که از نسخه 18 در دسترس است، بر پایه undici ساخته شده است؛ undici یک کلاینت HTTP است که درون هر نسخه Node.js عرضه می‌شود. استاندارد Fetch می‌گوید درخواستی که به خطای شبکه ختم شود با یک TypeError رد (reject) می‌شود. undici برای همه این خطاها یک پیام ثابت به کار می‌برد، یعنی fetch failed، و خطای اصلی را به‌عنوان cause به آن می‌افزاید.

خطای شبکه هر چیزی است که پیش از رسیدن پاسخ خراب شود: جست‌وجوی نام، اتصال TCP، دست‌دادن TLS، تونل پروکسی یا اتصالی که پیش از رسیدن هدرها بسته شود. خطای 404 یا 500 خطای شبکه نیست؛ fetch به‌طور عادی پاسخ را برمی‌گرداند و res.ok برابر false است.

وقتی هیچ کدی خطا را نگیرد، Node.js علت را خودش چاپ می‌کند. این خروجی واقعی یک اسکریپت یک‌خطی با نام میزبان غلط روی ⁦Node.js 26.10.0⁩ است:

text
[TypeError: fetch failed] {
  [cause]: Error: getaddrinfo ENOTFOUND api.example-typo.invalid
      at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:122:26) {
    errno: -3008,
    code: 'ENOTFOUND',
    syscall: 'getaddrinfo',
    hostname: 'api.example-typo.invalid'
  }
}

مشکل وقتی شروع می‌شود که کد خطا را بگیرد و فقط پیام را لاگ کند، همان کاری که بسیاری از چارچوب‌ها (فریم‌ورک‌ها) و SDKها انجام می‌دهند. چاپ علت فقط یک سطر بیشتر لازم دارد:

js
try {
  await fetch("https://api.example-typo.invalid/v1/items");
} catch (err) {
  console.log(err.message);                         // fetch failed
  console.log(err.cause?.code, err.cause?.message); // ENOTFOUND getaddrinfo ENOTFOUND api.example-typo.invalid
}

چگونه خطای واقعی پشت fetch failed را پیدا کنیم؟

  1. به‌جای err.message، مقدار err.cause را لاگ کنید. اگر چارچوبی آن را پنهان می‌کند، فراخوانی را خودتان در یک try/catch بپیچید.
  2. cause.code را بخوانید. کدهایی که با E شروع می‌شوند (ENOTFOUND، ECONNRESET) از سیستم‌عامل می‌آیند، کدهایی که با UND_ERR_ شروع می‌شوند از undici، و کدهایی مانند UNABLE_TO_GET_ISSUER_CERT_LOCALLY از لایه TLS.
  3. دنبال AggregateError بگردید. برای localhost، Node.js چند آدرس را امتحان می‌کند. در آزمون ما cause.message خالی بود و cause.errors یک ECONNREFUSED برای ::1 و یکی برای 127.0.0.1 داشت.
  4. در خطاهای پروکسی یک سطح عمیق‌تر بروید. ورود ردشده به پروکسی پیام Request was cancelled. را بدون کد داد؛ کد وضعیت در err.cause.cause بود: Proxy response (407) !== 200 when HTTP Tunneling.
  5. به زمان دقت کنید. چند میلی‌ثانیه یعنی رد شدن اتصال، بازنشانی یا پاسخ DNS؛ حدود 10 ثانیه یعنی مهلت اتصال؛ پنج دقیقه یعنی مهلت پیش‌فرض انتظار برای هدرها.

کدهای ⁦err.cause⁩: معنای هرکدام و آنچه باید رفع کنید

همه ردیف‌ها را روی ویندوز 11 با ⁦Node.js 24.21.0⁩ و 26.10.0 تولید کردیم، با یک پروکسی آزمون محلی و سرورهای محلی که اتصال را رد یا بازنشانی می‌کنند، معطل می‌مانند یا گواهی آزمایشی ارائه می‌دهند؛ هر دو نسخه کدهای یکسانی دادند. مرجع خطاهای undici کدهای UND_ERR_ را مستند کرده و توصیه می‌کند به‌جای instanceof با error.code مقایسه کنید، چون dispatcher ممکن است از نسخه دیگری از undici آمده باشد.

cause.code و پیامچه رخ دادنخست چه را بررسی کنید
ENOTFOUND ⁦getaddrinfo ENOTFOUND host⁩نام میزبان به آدرسی تبدیل نمی‌شوداملا، DNS، VPN
ECONNREFUSED ⁦connect ECONNREFUSED 127.0.0.1:8999⁩هیچ برنامه‌ای روی آن پورت منتظر نیستسرویس، پورت، پورت پروکسی
ECONNREFUSED درون یک AggregateErrorهمان، برای همه آدرس‌های localhostسرور را راه بیندازید
UND_ERR_CONNECT_TIMEOUT ⁦Connect Timeout Error⁩ظرف 10 ثانیه اتصال TCP برقرار نشدآدرس، فایروال
ECONNRESET ⁦read ECONNRESET⁩با بازنشانی TCP قطع شدپروکسی، فایروال؛ تلاش دوباره
UND_ERR_SOCKET ⁦other side closed⁩پیش از هر پاسخی بسته شدلاگ‌های سرور؛ تلاش دوباره
UND_ERR_HEADERS_TIMEOUT ⁦Headers Timeout Error⁩وصل شد، اما هدرها به‌موقع نرسیدندمحدودیت زمانی خودتان
UNABLE_TO_GET_ISSUER_CERT_LOCALLY، SELF_SIGNED_CERT_IN_CHAINگواهی ریشه مورد اعتماد نیستگواهی ریشه شرکت
UNABLE_TO_VERIFY_LEAF_SIGNATUREسرور گواهی میانی را نفرستادزنجیره گواهی سرور
ERR_SSL_WRONG_VERSION_NUMBERTLS به یک پورت HTTP ساده فرستاده شدhttps:// در آدرس پروکسی
UND_ERR_INVALID_ARG ⁦invalid onError method⁩agent از نسخه اصلی دیگری از undicifetch خود undici
بدون کد: ⁦Request was cancelled.⁩پروکسی تونل را رد کرداطلاعات ورود پروکسی

یک پیام مرتبط fetch failed نیست: اگر هدرها برسند اما بدنه معطل بماند، res.text() یا res.json() خطای TypeError: terminated را پرتاب می‌کند که در آزمون ما علتش UND_ERR_BODY_TIMEOUT بود.

⁦ENOTFOUND⁩ و ⁦ECONNREFUSED⁩: درخواست هرگز به سروری نرسید

ENOTFOUND از جست‌وجوی نام می‌آید: سرور DNS (resolver) پاسخ داده که چنین نامی وجود ندارد. دنبال غلط املایی، یک VPN با سرورهای DNS مخصوص خودش یا کانتینری بگردید که به DNS داخلی شرکت شما دسترسی ندارد. پشت پروکسی، نام را خود پروکسی پیدا می‌کند، پس میزبان نادرست به شکل کد وضعیت پروکسی برمی‌گردد: پروکسی آزمون ما 502 داد که به شکل Proxy response (502) !== 200 when HTTP Tunneling گزارش شد.

ECONNREFUSED یعنی ماشین پاسخ داده، اما هیچ برنامه‌ای روی آن پورت اتصال نمی‌پذیرد. اگر آدرس درون پیام آدرس پروکسی شماست، تنظیم پروکسی نادرست است، نه مقصد. با localhost، Node.js هم ::1 و هم 127.0.0.1 را امتحان می‌کند؛ در آزمون ما سروری که فقط روی 127.0.0.1 گوش می‌داد همچنان به http://localhost پاسخ داد. وقتی هیچ‌کدام پاسخ نمی‌دهند، سرور توسعه خاموش است، از پورت دیگری استفاده می‌کند، یا کد شما در Docker اجرا می‌شود که در آن localhost خود کانتینر است.

⁦ECONNRESET⁩، ⁦"other side closed"⁩ و ⁦"socket hang up"⁩

هر سه یعنی اتصالی باز شده و سپس قطع شده است. fetch را با ماژول http و ⁦Axios 1.20⁩ مقایسه کردیم:

  • سروری که با بازنشانی TCP پاسخ داد، در هر سه به read ECONNRESET انجامید.
  • سروری که اتصال را بدون پاسخ بست، در fetch به UND_ERR_SOCKET (other side closed) و در http و Axios به socket hang up با کد ECONNRESET انجامید.

دلیل‌های رایج: سرور در میانه درخواست از کار افتاده، یک متعادل‌کننده بار (load balancer) یا پروکسی یک اتصال keep-alive بیکار را، یعنی اتصالی که برای درخواست‌های بعدی باز نگه داشته می‌شود، درست در لحظه‌ای بسته که کلاینت شما دوباره از آن استفاده کرده، یا فایروالی یک اتصال طولانی را قطع کرده است. برخی سرورها و فیلترهای ربات هم اتصال‌هایی را که نمی‌خواهند به آن‌ها پاسخ دهند می‌بندند؛ معنایش این است که سرعت را کم کنید یا اجازه دسترسی بخواهید، نه اینکه پافشارانه‌تر تلاش کنید.

درخواست‌های idempotent، یعنی درخواست‌هایی که تکرارشان نتیجه را تغییر نمی‌دهد (GET و HEAD)، را یکی دو بار با تأخیری فزاینده دوباره بفرستید. درخواست POST را که سفارشی ثبت می‌کند کورکورانه تکرار نکنید: ممکن است سرور پیش از قطع اتصال کار را انجام داده باشد.

مهلت‌ها: ⁦UND_ERR_CONNECT_TIMEOUT⁩، ⁦UND_ERR_HEADERS_TIMEOUT⁩ و ⁦AbortSignal.timeout()⁩

fetch در Node.js هیچ محدودیت زمانی کلی ندارد. undici پس از 10 ثانیه از برقراری اتصال TCP دست می‌کشد، سپس تا 300 ثانیه برای هدرها و تا 300 ثانیه میان تکه‌های بدنه صبر می‌کند. سروری که اتصال را بپذیرد و معطل بماند می‌تواند یک درخواست را پنج دقیقه نگه دارد. محدودیت خودتان را تعیین کنید:

js
try {
  const res = await fetch("https://api.example.com/v1/items", { signal: AbortSignal.timeout(5_000) });
  console.log(res.status);
} catch (err) {
  if (err.name === "TimeoutError") console.log("gave up after 5 s");
  else console.log(err.message, err.cause?.code);
}

در برابر یک سرور محلی که هرگز پاسخ نمی‌دهد، این کد روی ⁦Node.js 24⁩ و 26 عبارت gave up after 5 s را چاپ کرد. همان‌طور که صفحه MDN درباره ⁦AbortSignal.timeout()⁩ (به انگلیسی) می‌گوید، سیگنال با یک TimeoutError لغو می‌شود، نه با fetch failed، پس err.name را بررسی کنید. این محدودیت بدنه را هم در بر می‌گیرد: بدنه‌ای که معطل ماند باعث شد res.text() همان TimeoutError را پرتاب کند. تغییر محدودیت‌های خود undici به یک Agent از بسته npm نیاز دارد و این، مسئله نسخه را پیش می‌کشد که در ادامه به آن می‌پردازیم.

چرا fetch متغیرهای ⁦HTTP_PROXY⁩ و ⁦HTTPS_PROXY⁩ را نادیده می‌گیرد؟

curl، pip و بسیاری از ابزارهای دیگر متغیرهای محیطی پروکسی را می‌خوانند؛ fetch در Node.js نمی‌خواند. با تنظیم HTTPS_PROXY، fetch روی ⁦Node.js 22.23.3⁩، 24.21.0 و 26.10.0 مستقیم به مقصد رفت و لاگ پروکسی ما خالی ماند، در حالی که curl از پروکسی استفاده کرد. جایی که فقط پروکسی به اینترنت دسترسی دارد، علت خطا مقصد را نام می‌برد (ENOTFOUND، UND_ERR_CONNECT_TIMEOUT)، هرگز پروکسی‌ای را که کنار گذاشته شده است.

پشتیبانی داخلی Node.js از پروکسی این رفتار را روشن می‌کند: NODE_USE_ENV_PROXY=1 (⁦Node.js 22.21.0⁩ و 24.0.0 یا بالاتر) یا پرچم --use-env-proxy (22.21.0 و 24.5.0 یا بالاتر). در این حالت Node.js متغیرهای HTTP_PROXY، HTTPS_PROXY و NO_PROXY را هنگام شروع می‌خواند، هم برای fetch و هم برای ماژول‌های http و https. مستندات هنوز این قابلیت را «در حال توسعه فعال» علامت زده است.

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

در PowerShell:

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

سپس پروکسی آزمون ما روی هر سه نسخه CONNECT example.com:443 را ثبت کرد؛ ⁦Node.js 22⁩ همچنین هشدار داد که EnvHttpProxyAgent آزمایشی است. سه جزئیات بسیاری را غافلگیر می‌کند:

  • آدرس پروکسی با http:// شروع می‌شود، حتی برای مقصدهای https://. درخواست تونل، HTTP ساده است و TLS درون آن اجرا می‌شود؛ https:// در ابتدای آدرس یک پروکسی HTTP ساده به ERR_SSL_WRONG_VERSION_NUMBER انجامید.
  • http.setGlobalProxyFromEnv() همین کار را از درون کد انجام می‌دهد، از ⁦Node.js 24.14.0⁩ و 25.4.0 به بعد؛ ⁦Node.js 22⁩ آن را ندارد.
  • رمز نادرست یک سطح عمیق‌تر پنهان می‌شود، در err.cause.cause.

آدرس اتصال (gateway) یک ارائه‌دهنده، مانند پروکسی مسکونی ما، به شکل http://user:pass@pr.proxynet.io:8000 در همان متغیر قرار می‌گیرد، یا وقتی IP سرور شما در لیست سفید (whitelist) باشد، بدون اطلاعات ورود.

برای تعیین پروکسی به تفکیک درخواست، Axios و ⁦SOCKS5⁩، راهنمای ما درباره استفاده از پروکسی در Node.js را دنبال کنید.

⁦Node.js 26⁩ و ⁦"invalid onError method"⁩: ناسازگاری نسخه undici

هر نسخه Node.js undici مخصوص خودش را همراه دارد که از بسته undici در npm جداست: ⁦Node.js 22.23.3⁩ همراه ⁦undici 6.28.1⁩، ⁦Node.js 24.21.0⁩ همراه 7.29.1 و ⁦Node.js 26.10.0⁩ همراه 8.10.2 عرضه می‌شود. ⁦undici 8.0.0⁩ پوشش‌هایی (wrapper) را حذف کرد که به کد handler قدیمی‌تر اجازه می‌داد با کد تازه‌تر کار کند. بر اساس برنامه انتشار Node.js، ⁦Node.js 26⁩ در 28 اکتبر 2026 شاخه Active LTS (پشتیبانی بلندمدت فعال) می‌شود، پس بسیاری از پروژه‌ها هنگام ارتقا با این مشکل روبه‌رو خواهند شد.

این خطا وقتی ظاهر می‌شود که یک ProxyAgent یا Agent از بسته npm به‌عنوان dispatcher به fetch سراسری داده شود. چهار نسخه اصلی (major) از بسته npm را روی سه نسخه Node.js آزمودیم:

undici در npm⁦Node.js 22.23.3⁩⁦Node.js 24.21.0⁩⁦Node.js 26.10.0⁩
5.29.0کار می‌کندکار می‌کندinvalid onError method
6.29.0کار می‌کندکار می‌کندinvalid onError method
7.30.0کار می‌کندکار می‌کندکار می‌کند
8.11.2invalid onRequestStart methodinvalid onRequestStart methodکار می‌کند

هر شکست TypeError: fetch failed بود، با UND_ERR_INVALID_ARG به‌عنوان علت. گونه بی‌صداتر بدتر است: روی ⁦Node.js 26⁩، setGlobalDispatcher(new ProxyAgent(...)) از ⁦undici 5⁩ یا 6 هیچ خطایی نداد، fetch آن را نادیده گرفت و درخواست مستقیم بیرون رفت، در حالی که پروکسی ما چیزی ندید.

راه رفع این است که fetch را از همان بسته‌ای بردارید که agent را از آن برداشته‌اید:

js
import { fetch, ProxyAgent } from "undici"; // fetch and the agent from the same package

const proxy = new ProxyAgent("http://user:pass@pr.proxynet.io:8000");
const res = await fetch("https://example.com/", { dispatcher: proxy });
console.log(res.status); // 200 in our test, through a local test proxy

این روش با ⁦undici 8.11.2⁩ روی ⁦Node.js 24⁩ و 26 کار کرد. اگر ناسازگاری در ابزاری است که خودتان ننوشته‌اید، مثلاً یک CLI که undici قدیمی را درون خود دارد و از متغیرهای پروکسی شما agent می‌سازد، ابزار را به‌روز کنید یا تا رفع مشکل، آن را روی ⁦Node.js 24⁩ نگه دارید.

خطاهای گواهی پشت fetch failed

بررسی ناموفق TLS هم به شکل fetch failed می‌رسد و کد گواهی در cause.code قرار دارد. در محیط کار، منشأ معمول یک پروکسی شرکتی یا آنتی‌ویروس است که ترافیک HTTPS را بازرسی می‌کند و آن را با گواهی ریشه خودش دوباره امضا می‌کند. Node.js گواهی‌ها را با فهرست داخلی خودش از مراجع ریشه بررسی می‌کند، نه با فهرست سیستم‌عامل، پس آن گواهی ریشه را با UNABLE_TO_GET_ISSUER_CERT_LOCALLY یا SELF_SIGNED_CERT_IN_CHAIN رد می‌کند.

متغیر NODE_EXTRA_CA_CERTS را به یک فایل PEM حاوی گواهی ریشه شرکت اشاره دهید، یا اگر گواهی ریشه روی ماشین نصب است، Node.js را با --use-system-ca اجرا کنید (از 22.15.0 و 23.8.0 به بعد). در آزمون ما NODE_EXTRA_CA_CERTS حالت نخست را رفع کرد اما UNABLE_TO_VERIFY_LEAF_SIGNATURE را نه؛ در آن حالت سرور گواهی میانی خود را نمی‌فرستد و فقط صاحب سرور می‌تواند آن را درست کند. راه‌حل‌ها برای npm، Git، Python و curl در خطای ⁦Unable to Get Local Issuer Certificate⁩ آمده است.

تابعی پوششی برای fetch که علت را نام می‌برد و دوباره تلاش می‌کند

این اسکریپت از fetch سراسری استفاده می‌کند، پس به هیچ بسته‌ای نیاز ندارد. کل زنجیره علت‌ها را پیمایش می‌کند، هر تلاش را با AbortSignal.timeout() محدود می‌کند و فقط بازنشانی‌ها، سوکت‌های بسته‌شده و پایان مهلت‌ها را دوباره امتحان می‌کند، با انتظار نمایی (exponential backoff)، یعنی فاصله‌ای که در هر تلاش دو برابر می‌شود، به‌علاوه یک جزء تصادفی (jitter). شکست DNS، رد شدن اتصال، خطای گواهی و ورود ردشده به پروکسی هر بار به همان شکل تکرار می‌شوند، پس اسکریپت روی آن‌ها متوقف می‌شود. در پاسخ به 429 و 503 منتظر Retry-After می‌ماند.

پاسخ 429 یعنی سرور از شما می‌خواهد سرعت را کم کنید؛ معنای این هدر و شیوه تنظیم سرعت یک کار را در خطای ⁦HTTP 429 Too Many Requests⁩ توضیح داده‌ایم.

js
// fetch-check.mjs: name the real cause behind "TypeError: fetch failed" and retry only what can recover.
// Tested on Node.js 22, 24 and 26. Optional proxy: NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://user:pass@pr.proxynet.io:8000

const RETRY_CODES = new Set(["ECONNRESET", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT"]);
const CERT_CODES = /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/** The error and every error wrapped inside it, outermost first. */
function causes(err) {
  const chain = [];
  for (let e = err; e && chain.length < 8; e = e.cause) {
    chain.push(e);
    if (e instanceof AggregateError) chain.push(...e.errors); // "localhost": one error per address
  }
  return chain;
}

/** The first string error code in the chain, such as "ECONNRESET". */
const codeOf = (err) => causes(err).map((e) => e.code).find((c) => typeof c === "string") ?? "";

/** One line: what failed and what to check first. */
function explain(err) {
  if (err.name === "TimeoutError") return "no answer before our AbortSignal.timeout(): slow server or proxy";
  const chain = causes(err);
  const text = chain.map((e) => e.message).join(" | ");
  const code = codeOf(err);
  const tunnel = text.match(/Proxy response \((\d{3})\)/);
  if (tunnel?.[1] === "407") return "proxy wants credentials (407): check user:pass or the IP whitelist";
  if (tunnel) return `proxy refused the tunnel (${tunnel[1]}): the proxy could not or would not reach the target`;
  if (code === "ENOTFOUND") return `name ${chain.find((e) => e.hostname)?.hostname} does not resolve: typo, DNS or VPN`;
  if (code === "ECONNREFUSED") return "nothing listens on that address and port: wrong port, service down or wrong proxy port";
  if (code === "ECONNRESET") return "the connection was cut (TCP reset): server, proxy or firewall dropped it";
  if (code === "UND_ERR_SOCKET") return "the other side closed the connection before answering";
  if (code === "UND_ERR_CONNECT_TIMEOUT") return "no TCP connection within 10 s: wrong IP, firewall or blocked outbound port";
  if (code === "UND_ERR_HEADERS_TIMEOUT") return "connected, but no response headers in time";
  if (code === "ERR_SSL_WRONG_VERSION_NUMBER") return "TLS spoken to a plain-HTTP port: the proxy URL should start with http://";
  if (CERT_CODES.test(code)) return `certificate not trusted (${code}): add your CA with NODE_EXTRA_CA_CERTS`;
  return `unrecognised, read the innermost error: ${chain.at(-1).message}`;
}

/** GET with a hard time limit, backoff for network blips, and Retry-After for 429/503. */
async function getWithRetry(url, { attempts = 3, timeoutMs = 15_000 } = {}) {
  for (let attempt = 1; ; attempt++) {
    const backoff = 500 * 2 ** (attempt - 1) + Math.random() * 250; // 0.5 s, 1 s, 2 s ... plus jitter
    let res;
    try {
      res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
    } catch (err) {
      const code = codeOf(err);
      const retryable = RETRY_CODES.has(code) || err.name === "TimeoutError";
      if (!retryable || attempt === attempts) throw err;
      console.log(`  attempt ${attempt}: ${code || err.name}, retrying in ${Math.round(backoff)} ms`);
      await sleep(backoff);
      continue;
    }
    if ((res.status === 429 || res.status === 503) && attempt < attempts) {
      const wait = Number(res.headers.get("retry-after")) * 1000 || backoff; // seconds form only
      await res.body?.cancel();
      console.log(`  attempt ${attempt}: HTTP ${res.status}, waiting ${Math.round(wait)} ms`);
      await sleep(wait);
      continue;
    }
    return res;
  }
}

for (const url of process.argv.slice(2)) {
  console.log(url);
  try {
    const res = await getWithRetry(url, { timeoutMs: 5_000 });
    console.log(`  OK: HTTP ${res.status}`);
  } catch (err) {
    console.log(`  FAILED: ${err.name}: ${err.message} -> ${explain(err)}`);
  }
}

خروجی چه شکلی دارد

اسکریپت را روی ⁦Node.js 26.10.0⁩ در برابر یک نام میزبان غلط، یک پورت بسته روی localhost و سرورهای محلی اجرا کردیم که هر اتصال را بازنشانی می‌کنند (پورت 8902)، هرگز پاسخ نمی‌دهند (8901)، گواهی صادرشده از مرجع آزمایشی خودمان را به کار می‌برند (8906) و دو بار 429 پاسخ می‌دهند و سپس موفق می‌شوند (8910):

text
https://api.example-typo.invalid/
  FAILED: TypeError: fetch failed -> name api.example-typo.invalid does not resolve: typo, DNS or VPN
http://localhost:8999/
  FAILED: TypeError: fetch failed -> nothing listens on that address and port: wrong port, service down or wrong proxy port
http://127.0.0.1:8902/
  attempt 1: ECONNRESET, retrying in 652 ms
  attempt 2: ECONNRESET, retrying in 1115 ms
  FAILED: TypeError: fetch failed -> the connection was cut (TCP reset): server, proxy or firewall dropped it
http://127.0.0.1:8901/
  attempt 1: TimeoutError, retrying in 658 ms
  attempt 2: TimeoutError, retrying in 1024 ms
  FAILED: TimeoutError: The operation was aborted due to timeout -> no answer before our AbortSignal.timeout(): slow server or proxy
https://127.0.0.1:8906/
  FAILED: TypeError: fetch failed -> certificate not trusted (UNABLE_TO_GET_ISSUER_CERT_LOCALLY): add your CA with NODE_EXTRA_CA_CERTS
http://127.0.0.1:8910/items
  attempt 1: HTTP 429, waiting 1000 ms
  attempt 2: HTTP 429, waiting 1000 ms
  OK: HTTP 200

سپس از طریق پروکسی آزمون محلی با NODE_USE_ENV_PROXY=1: با رمز درست، با رمز نادرست، و با https:// در ابتدای آدرس پروکسی:

text
https://example.com/
  OK: HTTP 200
https://example.com/
  FAILED: TypeError: fetch failed -> proxy wants credentials (407): check user:pass or the IP whitelist
https://example.com/
  FAILED: TypeError: fetch failed -> TLS spoken to a plain-HTTP port: the proxy URL should start with http://

⁦Node.js 22.23.3⁩ و 24.21.0 همین سطرها را چاپ کردند، جز مقدارهای تصادفی زمان انتظار.

کجا با این خطا روبه‌رو می‌شوید

  • Next.js و دیگر چارچوب‌های سمت سرور، که صفحه‌های خطایشان اغلب فقط پیام را نشان می‌دهد.
  • سرورهای MCP و ابزارهای عامل هوش مصنوعی، که اغلب پشت VPN یا پروکسی شرکتی اجرا می‌شوند.
  • SDKهای ساخته‌شده بر پایه fetch، که برخی از آن‌ها در نسخه‌های قدیمی‌تر علت را دور می‌ریختند.
  • سرورهای CI و ساخت‌های Docker، که متغیرهای پروکسی و گواهی ریشه شرکتی ماشین میزبان را ندارند.
  • کارهای اسکرپینگ و پایش، که در اجراهای طولانی با بیشتر ردیف‌های جدول بالا روبه‌رو می‌شوند.

کارهای طولانی جمع‌آوری داده بیشترین سود را از این تابع پوششی می‌برند. اگر چنین کاری با چند مقصد مشخص سروکار دارد و به IPهای خروجی نیاز دارد که هفته‌ها ثابت بمانند، یک پروکسی دیتاسنتر آدرس‌های ⁦IPv4⁩ اختصاصی با ترافیک بدون سهمیه در اختیارتان می‌گذارد.

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

  • لاگ کردن err.message. همیشه می‌گوید fetch failed.
  • انتظار اینکه fetch متغیر HTTPS_PROXY را بخواند. بدون NODE_USE_ENV_PROXY=1 یا --use-env-proxy نمی‌خواند.
  • دادن agent بسته undici در npm به fetch سراسری. fetch را از همان بسته وارد کنید.
  • خاموش کردن بررسی گواهی. NODE_TLS_REJECT_UNAUTHORIZED=0 اعتبارسنجی را برای کل فرایند غیرفعال می‌کند، پس مهاجمی که در مسیر نشسته درست مانند پروکسی شرکت شما دیده می‌شود.
  • تلاش دوباره برای همه چیز. غلط املایی هر بار به همان شکل شکست می‌خورد؛ یک POST پس از ECONNRESET ممکن است دو بار اجرا شود.
  • نبود محدودیت زمانی. سروری که معطل بماند می‌تواند یک درخواست را پنج دقیقه نگه دارد.

راهنمای انتخاب

آنچه در err.cause می‌بینیدچه کنید
ENOTFOUNDنام میزبان یا DNS را اصلاح کنید؛ تلاش دوباره لازم نیست
ECONNREFUSED با آدرس پروکسی شمامیزبان و پورت پروکسی را دوباره کپی کنید
ECONNREFUSED روی localhostسرور را راه بیندازید؛ پورت و Docker را بررسی کنید
ECONNRESET، UND_ERR_SOCKETدرخواست‌های GET را با انتظار نمایی دوباره بفرستید
UND_ERR_CONNECT_TIMEOUTفایروال را بررسی کنید؛ پشت پروکسی شرکتی NODE_USE_ENV_PROXY=1 را تنظیم کنید
انتظار طولانی و سپس UND_ERR_HEADERS_TIMEOUTAbortSignal.timeout() اضافه کنید
یک کد گواهیNODE_EXTRA_CA_CERTS یا --use-system-ca
Proxy response (407) یک سطح عمیق‌ترuser:pass یا لیست سفید IP را بررسی کنید
invalid onError methodfetch را از بسته undici همان agent وارد کنید

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

چرا fetch در پاسخ 404 یا 500 خطا پرتاب نمی‌کند؟

چون سرور پاسخ داده است. fetch فقط در خطاهای شبکه رد می‌شود؛ خطای HTTP به‌طور عادی برمی‌گردد و res.ok برابر false است، پس پیش از خواندن بدنه آن را بررسی کنید.

آیا ⁦"fetch failed"⁩ همان ⁦"Failed to fetch"⁩ در مرورگر است؟

به هم مربوط‌اند، اما یکی نیستند. کروم با TypeError: Failed to fetch رد می‌شود و دلیل را به صفحه نمی‌گوید، پس یک مسدودسازی CORS، یعنی قاعده مرورگر برای درخواست به دامنه‌ای دیگر، دقیقاً همین شکل را دارد؛ در DevTools زبانه Network (شبکه) را باز کنید. Node.js دلیل را در cause می‌گذارد.

چگونه برای fetch در Node.js مهلت زمانی تعیین کنم؟

signal: AbortSignal.timeout(ms) را بدهید. وقتی مهلت تمام شود، fetch با یک TimeoutError رد می‌شود و این محدودیت بدنه معطل‌مانده را هم متوقف می‌کند.

آیا fetch در Node.js از ⁦HTTP_PROXY⁩ و ⁦HTTPS_PROXY⁩ استفاده می‌کند؟

فقط با NODE_USE_ENV_PROXY=1 (22.21.0، 24.0.0 و بالاتر) یا --use-env-proxy (22.21.0، 24.5.0 و بالاتر). در غیر این صورت این متغیرها بی‌صدا نادیده گرفته می‌شوند.

چرا curl کار می‌کند اما fetch روی همان ماشین شکست می‌خورد؟

معمولاً به یکی از دو دلیل: curl متغیرهای پروکسی را می‌خواند و fetch نه، و بسیاری از نسخه‌های curl، از جمله نسخه درون ویندوز، از مخزن گواهی سیستم استفاده می‌کنند، در حالی که Node.js فهرست خودش را به کار می‌برد، مگر اینکه --use-system-ca را بدهید.

آیا پروکسی می‌تواند ⁦"TypeError: fetch failed"⁩ را رفع کند؟

فقط وقتی علت، مسیر شبکه است، مثلاً شبکه شرکتی که در آن فقط پروکسی به اینترنت دسترسی دارد. پروکسی غلط املایی، مشکل گواهی یا سروری را که خاموش است رفع نمی‌کند، و راهی برای گذشتن از سایتی نیست که اتصال شما را بازنشانی می‌کند یا نرخ درخواست‌هایتان را محدود می‌کند: سرعت را کم کنید، از API رسمی آن استفاده کنید یا اجازه دسترسی بخواهید.

خلاصه

TypeError: fetch failed یک پوشش است: دلیل در err.cause است، گاهی یک سطح عمیق‌تر. کد را بخوانید، آنچه را نام می‌برد رفع کنید، به هر فراخوانی یک AbortSignal.timeout() بدهید و فقط بازنشانی‌ها و پایان مهلت‌ها را دوباره امتحان کنید. پشت پروکسی، NODE_USE_ENV_PROXY=1 را تنظیم کنید، http:// را در آدرس پروکسی نگه دارید و fetch و agent آن را از یک بسته undici بردارید، به‌ویژه روی ⁦Node.js 26⁩. وقتی کد شما برای پروکسی آماده شد، گزینه‌ها را در صفحه خدمات پروکسی ما مقایسه کنید.

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