---
title: "خطای ⁦TypeError: fetch failed⁩ در Node.js: علت و راه رفع"
description: "خطای TypeError: fetch failed در Node.js علت واقعی را در err.cause پنهان می‌کند. خواندن آن و رفع علت‌های DNS، اتصال، مهلت زمانی، گواهی و پروکسی را بیاموزید."
url: https://proxynet.io/fa/blog/typeerror-fetch-failed
date: 2026-10-06
author: "Acar Diveroli"
category: "آموزش‌ها, وب اسکرپینگ"
lang: fa
---

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

اسکریپت 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 برای هر شکستی پیش از رسیدن پاسخ HTTP پرتاب می‌کند؛ علت در `err.cause` است، پس به‌جای `err.message` آن را لاگ کنید. `ENOTFOUND` یعنی نام میزبان به هیچ آدرس IP تبدیل نشد، `ECONNREFUSED` یعنی هیچ برنامه‌ای روی آن پورت منتظر نیست، `ECONNRESET` یا `UND_ERR_SOCKET` یعنی اتصال قطع شد و `UND_ERR_CONNECT_TIMEOUT` یعنی ظرف 10 ثانیه هیچ اتصالی باز نشد. پشت پروکسی، fetch متغیر `HTTPS_PROXY` را نادیده می‌گیرد، مگر اینکه Node.js با `NODE_USE_ENV_PROXY=1` اجرا شود. `invalid onError method` یعنی یک agent از بسته undici در npm با undici درون خود Node.js جور نیست.

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

تابع سراسری `fetch()` در Node.js، که از نسخه 18 در دسترس است، بر پایه undici ساخته شده است؛ undici یک کلاینت HTTP است که درون هر نسخه Node.js عرضه می‌شود. [استاندارد Fetch](https://fetch.spec.whatwg.org/#fetch-method) می‌گوید درخواستی که به خطای شبکه ختم شود با یک `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](https://github.com/nodejs/undici/blob/main/docs/docs/api/Errors.md) کدهای `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_NUMBER` | TLS به یک پورت HTTP ساده فرستاده شد | `https://` در آدرس پروکسی |
| `UND_ERR_INVALID_ARG` ⁦invalid onError method⁩ | agent از نسخه اصلی دیگری از undici | `fetch` خود 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()⁩](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static) (به انگلیسی) می‌گوید، سیگنال با یک `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 از پروکسی](https://nodejs.org/api/http.html#built-in-proxy-support) این رفتار را روشن می‌کند: `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) یک ارائه‌دهنده، مانند [پروکسی مسکونی](https://proxynet.io/fa/residential-proxy) ما، به شکل `http://user:pass@pr.proxynet.io:8000` در همان متغیر قرار می‌گیرد، یا وقتی IP سرور شما در لیست سفید (whitelist) باشد، بدون اطلاعات ورود.

برای تعیین پروکسی به تفکیک درخواست، Axios و ⁦SOCKS5⁩، راهنمای ما درباره [استفاده از پروکسی در Node.js](/fa/blog/nodejs-proxy) را دنبال کنید.

## ⁦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](https://github.com/nodejs/Release)، ⁦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.2 | invalid onRequestStart method | invalid 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⁩](/fa/blog/unable-to-get-local-issuer-certificate) آمده است.

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

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

پاسخ `429` یعنی سرور از شما می‌خواهد سرعت را کم کنید؛ معنای این هدر و شیوه تنظیم سرعت یک کار را در [خطای ⁦HTTP 429 Too Many Requests⁩](/fa/blog/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های خروجی نیاز دارد که هفته‌ها ثابت بمانند، یک [پروکسی دیتاسنتر](https://proxynet.io/fa/datacenter-proxy) آدرس‌های ⁦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_TIMEOUT` | `AbortSignal.timeout()` اضافه کنید |
| یک کد گواهی | `NODE_EXTRA_CA_CERTS` یا `--use-system-ca` |
| `Proxy response (407)` یک سطح عمیق‌تر | user:pass یا لیست سفید IP را بررسی کنید |
| `invalid onError method` | `fetch` را از بسته 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⁩. وقتی کد شما برای پروکسی آماده شد، گزینه‌ها را در صفحه [خدمات پروکسی ما](/fa/proxy) مقایسه کنید.
