بار اول که این خطا را در یک پروژه جدی دیدم، در یک فروشگاه اینترنتی بود که سرویس پرداختش روی یک پورت اختصاصی کار می‌کرد و یک روز صبح، همه کاربران در مرحله پرداخت با پیام قرمز net::ERR_CONNECTION_REFUSED متوقف می‌شدند. تشخیص اولیه به سمت کد جاوااسکریپت رفت، ولی وقتی همان درخواست را از curl زدم و همان خطا را گرفتم، فهمیدم مسئله از پایه در لایه شبکه است. از آن روز، هر بار این خطا را در کنسول می‌بینم، پیش از هر چیز سراغ سرویس مقصد می‌روم، نه سراغ کد.

خطای ERR_CONNECTION_REFUSED دقیقاً چیست؟

خطای net::ERR_CONNECTION_REFUSED یکی از پیام‌های استاندارد مرورگرهای مبتنی بر موتور Chromium است که زمانی ظاهر می‌شود که کلاینت تلاش می‌کند با یک سرور مقصد روی یک پورت مشخص ارتباط برقرار کند، ولی سرور با یک بسته TCP از نوع RST (Reset) به‌صراحت اعلام می‌کند که این ارتباط را نمی‌پذیرد. به بیان ساده، این خطا یعنی درخواست شما به مقصد رسیده، ولی مقصد درِ خانه را بسته است.

نکته‌ای که در نگاه اول پنهان می‌ماند این است که این خطا یک خطای جاوااسکریپت نیست؛ بلکه یک خطای شبکه‌ای است که در قالب یک رویداد مرورگر، در کنسول نمایش داده می‌شود. با این حال، وقتی جاوااسکریپت با Transmission Control Protocol کار می‌کند و درخواستی می‌فرستد، در صورت مواجهه با این وضعیت، یک TypeError: Failed to fetch یا یک NetworkError دریافت می‌کند. تفاوت این دو پیام، یکی از پرتکرارترین منابع اشتباه در دیباگ است.

در مستندات رسمی Chromium، این خطا در دسته net:: قرار می‌گیرد که مختص خطاهای لایه شبکه است. پیام‌های دیگر این دسته شامل net::ERR_CONNECTION_TIMED_OUT، net::ERR_NAME_NOT_RESOLVED، net::ERR_CONNECTION_RESET و net::ERR_INTERNET_DISCONNECTED هستند. هر کدام از این‌ها به یک لایه متفاوت از پشته شبکه اشاره می‌کنند و همین تفکیک، اولین گام در تشخیص ریشه است.

ERR_CONNECTION_REFUSED یعنی مقصد درخواست شما را دید، ولی از پذیرفتنش خودداری کرد؛ این با «مقصد را پیدا نکردم» تفاوت بنیادی دارد.

اگر با خانواده خطاهای جاوااسکریپت آشنایی کامل ندارید، پیشنهاد می‌کنم ابتدا مقدمات را در «آموزش جاوااسکریپت از صفر» مرور کنید؛ چون درک این خطا بدون درک چرخه درخواست شبکه در جاوااسکریپت، سطحی می‌ماند.

لایه پایین: مکانیزم TCP و مفهوم refuse

برای این‌که بتوانید این خطا را در ریشه رفع کنید، باید مکانیزم پایین‌دستی آن را بلد باشید. TCP (Transmission Control Protocol) یک پروتکل مبتنی بر ارتباط است و برای هر اتصال، سه‌مرحله‌ای به‌نام three-way handshake طی می‌کند. کلاینت ابتدا یک بسته SYN می‌فرستد؛ سرور با SYN-ACK پاسخ می‌دهد؛ و کلاینت با ACK اتصال را تأیید می‌کند. این چرخه، درست پشت صحنه هر درخواست HTTP و HTTPS انجام می‌شود.

وقتی سرور مقصد روی یک پورت مشخص فعال نیست، سه واکنش ممکن است: اگر فایروال بسته را دور بریزد، کلاینت در حالت انتظار می‌ماند و در نهایت با ERR_CONNECTION_TIMED_OUT مواجه می‌شود. اگر فایروال با ICMP پاسخ دهد، ممکن است خطای دیگری ظاهر شود. ولی اگر خود سیستم‌عامل سرور روی آن پورت هیچ سرویس گوش‌دهنده (listening service) نداشته باشد، کرنل سرور به‌طور خودکار با یک بسته RST پاسخ می‌دهد و دقیقاً همین RST است که در مرورگر به شکل ERR_CONNECTION_REFUSED دیده می‌شود.

این تفکیک عملی، به‌شکل مستقیم روی رویکرد دیباگ شما اثر می‌گذارد. اگر خطا REFUSED است، شما باید سراغ سرویس مقصد بروید، نه سراغ شبکه. یعنی باید بررسی کنید که آیا سرویس مقصد روی آن پورت فعال است؟ آیا پروسه‌ای که باید گوش بدهد، درست اجرا شده؟ آیا با کرش کردن، پورت را رها کرده؟ در مقابل، اگر خطا TIMED_OUT باشد، مسئله فایروال یا مسیریابی است.

یک تکنیک کاربردی برای بررسی سریع، استفاده از دستورات خط فرمان است:

# بررسی باز بودن پورت روی سرور مقصد
telnet example.com 8080

# بررسی از دید سیستم‌عامل محلی
ss -tlnp | grep 8080

# تست از دید کلاینت با curl
curl -v http://localhost:8080/health

اگر telnet با پیام «Connection refused» برگردد، تشخیص قطعی است: سرویس مقصد روی آن پورت گوش نمی‌دهد. این تست، در تجربه من به‌شکل چشمگیری مسیر تشخیص را کوتاه می‌کند.

RST یک پیام صریح از سمت سرور است؛ یعنی «من هستم، ولی در این پورت گوش نمی‌دهم.» این با «سکوت و انتظار» تفاوت کامل دارد.

تفاوت با خطاهای شبکه‌ای دیگر

برای این‌که در پروژه‌های چندخطایی بتوانید سریع تشخیص دهید کدام خطای شبکه‌ای را پیش رو دارید، بد نیست اعضای پرتکرار این خانواده را کنار هم ببینید:

خطالایهمعنای دقیق
net::ERR_CONNECTION_REFUSEDلایه ۴ (TCP)سرور با RST پاسخ داد؛ سرویس گوش نمی‌دهد
net::ERR_CONNECTION_TIMED_OUTلایه ۴ (TCP)پاسخ نیامد؛ فایروال یا مسیریابی مشکل دارد
net::ERR_CONNECTION_RESETلایه ۴ (TCP)اتصال برقرار شد ولی وسط راه قطع شد
net::ERR_NAME_NOT_RESOLVEDلایه ۷ (DNS)نام دامنه پیدا نشد
net::ERR_INTERNET_DISCONNECTEDلایه ۳ (شبکه)اتصال شبکه محلی قطع است
net::ERR_SSL_PROTOCOL_ERRORلایه ۵ (TLS)دست‌دادن TLS ناموفق بود
net::ERR_CERT_DATE_INVALIDلایه ۵ (TLS)گواهی SSL منقضی شده

تفاوت REFUSED با TIMED_OUT در تجربه من یکی از پرتکرارترین موارد سردرگمی است. اگر سرویس مقصد کاملاً خاموش باشد و فایروال هم کاملاً بسته باشد، خطا معمولاً TIMED_OUT می‌شود؛ ولی اگر سیستم‌عامل سرور فعال است و فقط آن پورت گوش نمی‌دهد، خطا REFUSED می‌شود. به همین دلیل، در پرونده‌های واقعی، وجود REFUSED معمولاً نشانه دقیق‌تری است: یعنی شبکه سالم است، ولی سرویس مقصد مشکل دارد.

در لایه DNS، تفاوت با ERR_NAME_NOT_RESOLVED بسیار روشن است. اگر دامنه‌ای که درخواست می‌فرستید وجود نداشته باشد یا DNS نتواند آن را به IP تبدیل کند، خطا در همان لایه است. در این حالت، درخواست شما هرگز به لایه TCP نمی‌رسد. برای درک دقیق‌تر این خطا و تفاوتش با خطای فعلی، مرور «رفع خطای DNS در سرور» توصیه می‌شود؛ چون مرز بین این دو خطا در بافت پروژه‌های واقعی، یکی از پرتکرارترین موارد اشتباه در دیباگ است.

در لایه TLS، خطاهای مربوط به گواهی SSL مثل ERR_CERT_DATE_INVALID و ERR_SSL_PROTOCOL_ERROR ممکن است در نگاه اول شبیه خطای فعلی به نظر برسند، ولی ماهیتشان کاملاً متفاوت است. برای درک دقیق‌تر این خانواده خطا و مرز آن با خطاهای TCP، مرور «خطای SSL چیست و چگونه رفع می‌شود» و «SSL و HTTPS چه نقشی در امنیت دارند» توصیه می‌شود.

این خطا در مرورگر و در جاوااسکریپت چه تفاوتی دارد؟

یکی از نکات مهمی که در پرونده‌های دیباگ زیاد دیده‌ام، تفاوت بین خطای نمایش‌داده‌شده در نوار آدرس مرورگر و خطای دریافت‌شده در جاوااسکریپت است. وقتی شما یک آدرس را مستقیماً در نوار آدرس مرورگر وارد می‌کنید و مقصد پاسخ نمی‌دهد، مرورگر یک صفحه خطای داخلی نمایش می‌دهد با پیام ERR_CONNECTION_REFUSED. در این حالت، خطا در بالاترین لایه مرورگر شکل گرفته و کد جاوااسکریپت شما هیچ‌گاه اجرا نمی‌شود.

اما وقتی همین درخواست از درون کد جاوااسکریپت با fetch یا XMLHttpRequest اجرا می‌شود، خطا به‌شکل یک TypeError با پیام Failed to fetch یا NetworkError when attempting to fetch resource دریافت می‌شود. در این حالت، پیام دقیق ERR_CONNECTION_REFUSED در کنسول نمایش داده می‌شود، ولی شیء خطای دریافت‌شده در کد شما، همان جزئیات را ندارد. این تفاوت، یکی از پرتکرارترین منابع سردرگمی است.

// تلاش برای درخواست به سروری که در آن پورت گوش نمی‌دهد
try {
  const res = await fetch("http://localhost:9999/api/data");
  const data = await res.json();
} catch (error) {
  // error.name === "TypeError"
  // error.message === "Failed to fetch"
  // جزئیات ERR_CONNECTION_REFUSED در کنسول است، نه در error
  console.error(error);
}

نکته ظریف این است که در بعضی مرورگرها، جاوااسکریپت به دلایل امنیتی نمی‌تواند بین خطاهای شبکه‌ای مختلف تفکیک قائل شود. این محدودیت عامدانه است؛ اگر کد جاوااسکریپت می‌توانست بین «سرور پاسخ نداد» و «سرور پاسخ داد ولی با خطا» تفکیک کند، مهاجم می‌توانست از این تفاوت برای اسکن شبکه داخلی کاربران استفاده کند. به همین دلیل، همیشه در سمت جاوااسکریپت با یک خطای عمومی TypeError طرف هستید.

برای درک عمیق‌تر چرخه درخواست شبکه در جاوااسکریپت مدرن و تفاوت بین fetch و XMLHttpRequest در بافت خطا، مرور «fetch api در جاوااسکریپت» توصیه می‌شود. یکی از نکاتی که در آن متن باز شده، همین محدودیت مرورگرها در افشای جزئیات خطای شبکه است.

در مرورگر، پیام دقیق را می‌بینید ولی در کد خودتان فقط یک TypeError عمومی دریافت می‌کنید؛ این شکاف، عامدانه طراحی شده است.

دام بزرگ: وقتی CORS به‌جای خطای شبکه ظاهر می‌شود

یکی از پرتکرارترین پرونده‌هایی که در پروژه‌های واقعی دیده‌ام، زمانی است که تیم فنی به‌اشتباه این خطا را به مسئله CORS نسبت می‌دهد. جالب است بدانید که در بعضی سناریوها، خطای ERR_CONNECTION_REFUSED و خطای CORS در نگاه اول شبیه هم به نظر می‌رسند، ولی ریشه‌شان کاملاً متفاوت است.

در CORS، سرور مقصد فعال است و پاسخ می‌دهد، ولی مرورگر به‌دلیل نبود هدر Access-Control-Allow-Origin مناسب، پاسخ را از دسترس جاوااسکریپت خارج می‌کند. در این حالت، سرور پاسخ داده و فقط مرورگر تصمیم گرفته که آن پاسخ را به کد جاوااسکریپت ندهد. برخلاف این، در ERR_CONNECTION_REFUSED، سرور هیچ پاسخی نداده و اتصال در لایه TCP شکسته شده است.

تشخیص این دو، نیازمند یک تست ساده است: از curl یا ابزار مستقل از مرورگر برای فرستادن درخواست استفاده کنید. اگر curl پاسخ می‌گیرد ولی مرورگر خطا می‌دهد، مسئله احتمالاً CORS است. اگر curl هم خطای «Connection refused» می‌دهد، مسئله در لایه سرویس مقصد است.

# اگر CORS باشد، پاسخ دریافت می‌شود (با هدرهای CORS یا بدون)
curl -i -X OPTIONS -H "Origin: https://example.com" https://api.example.com/data

# اگر ERR_CONNECTION_REFUSED باشد، حتی curl هم پاسخ نمی‌گیرد
curl -v https://api.example.com/data

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

در بافت CORS، یکی از پرتکرارترین خطاهای جانبی، دامنه‌ای است که به‌اشتباه مسدود می‌شود و در مرورگر به‌شکل یک خطای شبکه ظاهر می‌شود. اگر می‌خواهید تفاوت دقیق این خانواده خطاها را ببینید، مرور «تفاوت REST و GraphQL» می‌تواند در بافت معماری API مفید باشد؛ چون در هر دو، CORS و خطاهای شبکه به‌شکل مشابهی مدیریت می‌شوند.

هشت سناریوی واقعی که این خطا را فعال می‌کنند

در پرونده‌هایی که به من رسیده، تعداد الگوهایی که به این خطا منتهی می‌شوند بیشتر از آن‌چه انتظار می‌رود است. هشت سناریوی زیر، تقریباً همه پرونده‌های عملی را پوشش می‌دهند.

سناریو اول: سرویس مقصد اصلاً اجرا نشده است

شایع‌ترین حالت. در سرور محلی، سرویس مقصد مثل node server.js اجرا نشده و پورت گوش نمی‌دهد. کد جاوااسکریپت سمت کلاینت، درخواست می‌فرستد، ولی هیچ سرویسی روی آن پورت منتظر نیست و کرنل با RST پاسخ می‌دهد. راه‌حل، اطمینان از اجرای سرویس مقصد پیش از اجرای کلاینت است.

سناریو دوم: سرویس کرش کرده ولی پورت آزاد نشده است

گاهی سرویس به دلیل خطای داخلی کرش می‌کند و پورت در وضعیت TIME_WAIT باقی می‌ماند. در این حالت، به‌نظر می‌رسد که پورت بسته است، ولی به‌طور واقعی گوش نمی‌دهد. راه‌حل، بررسی وضعیت پروسه و بستن پورت‌های بازمانده است. در سیستم‌های لینوکسی، ابزار ss -tlnp و lsof -i :port در این مرحله بسیار به کار می‌آید.

سناریو سوم: پورت اشتباه در آدرس درخواست

یکی از پرتکرارترین اشتباهات این است که سرویس روی یک پورت گوش می‌دهد (مثلاً ۳۰۰۰) ولی کد کلاینت به پورت دیگری (مثلاً ۸۰۸۰) درخواست می‌فرستد. این تخلف، به‌ویژه در پروژه‌های چندفایلی که پورت‌ها در فایل‌های تنظیمات متفاوت تعریف می‌شوند، بسیار شایع است. راه‌حل، مرکزیت دادن به تنظیمات پورت و اطمینان از خواندن آن‌ها در همه لایه‌ها است.

سناریو چهارم: فایروال سمت سرور پورت را بسته است

در بعضی هاست‌ها، فایروال سمت سرور (مثل ufw، iptables یا firewalld) پورت‌های غیراستاندارد را به‌طور پیش‌فرض می‌بندد. در این حالت، سرویس گوش می‌دهد، ولی بسته‌های TCP به آن نمی‌رسند. تفاوت این سناریو با سناریوی اول این است که اگر از داخل سرور با localhost تست کنید، پاسخ دریافت می‌کنید، ولی از بیرون، خطای REFUSED یا TIMED_OUT می‌گیرید. راه‌حل، باز کردن پورت در فایروال است:

sudo ufw allow 3000/tcp
sudo iptables -A INPUT -p tcp --dport 3000 -j ACCEPT

سناریو پنجم: هاست مقصد روی حالت تعمیرات است

در بعضی هاست‌ها، وقتی سایت به حالت تعمیرات می‌رود، پروکسی جلوی سایت، درخواست‌های API را رد می‌کند. این رفتار، در بعضی تنظیمات به‌شکل REFUSED ظاهر می‌شود. راه‌حل، بررسی وضعیت هاست از پنل کاربری و صبر کردن تا پایان تعمیرات است.

سناریو ششم: اشتباه در پروتکل (HTTP به‌جای HTTPS)

یکی از پرتکرارترین موارد سردرگمی این است که کد جاوااسکریپت به http://example.com:80 درخواست می‌فرستد، در حالی که سرور فقط روی https://example.com:443 گوش می‌دهد. اگر پورت ۸۰ در سرور بسته باشد، خطای REFUSED رخ می‌دهد. راه‌حل، اطمینان از تطابق پروتکل و پورت در کد و سرور است. برای درک دقیق‌تر نقش SSL در این بافت، مرور «چگونه SSL را نصب و فعال کنیم» و «تأثیر HTTPS بر سئو» توصیه می‌شود.

سناریو هفتم: Docker و شبکه‌های مجازی

در پروژه‌های مبتنی بر Docker، اگر کلاینت در یک container و سرویس مقصد در container دیگری باشد، پورت‌ها در شبکه داخلی Docker با پورت‌های هاست تفاوت دارند. اگر کلاینت به localhost درخواست بفرستد، به container خودش اشاره می‌کند، نه به container سرویس مقصد. راه‌حل، استفاده از نام container در شبکه داخلی یا انتشار صریح پورت در تنظیمات Docker است.

سناریو هشتم: تغییر IP یا DNS دامنه

وقتی IP سرور تغییر می‌کند ولی رکوردهای DNS هنوز به IP قدیمی اشاره می‌کنند، درخواست‌ها به سرور قدیمی می‌رسند که دیگر سرویس گوش نمی‌دهد و خطای REFUSED می‌دهند. این سناریو، مخصوصاً در زمان مهاجرت هاستینگ بسیار شایع است. برای درک دقیق‌تر روند مهاجرت و تنظیم رکوردهای DNS، مرور «چگونه هاست را به دامنه متصل کنیم» توصیه می‌شود.

در همه هشت سناریو، یک نکته مشترک وجود دارد: سرویس مقصد در آن پورت مشخص، آماده پذیرش اتصال نبوده است.

ریشه‌های سمت سرور و زیرساخت

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

لایه سرویس

اولین لایه‌ای که باید بررسی شود، خود سرویس است. آیا سرویس در حال اجرا است؟ آیا در فایل لاگ سرویس، خطایی ثبت شده؟ آیا سرویس روی همان پورتی که در تنظیمات تعریف شده، گوش می‌دهد؟ برای بررسی این لایه، ابزارهای زیر بسیار به کار می‌آیند:

# بررسی پروسه‌های فعال
ps aux | grep node

# بررسی لاگ سیستم
journalctl -u my-service -n 100

# بررسی پورت‌های گوش‌دهنده
ss -tlnp
netstat -tlnp

لایه فایروال

لایه دوم، فایروال است. حتی اگر سرویس به‌درستی اجرا شده باشد، ممکن است فایروال سرور یا فایروال شبکه، بسته‌های TCP را بلاک کند. تفاوت مهم این لایه با لایه سرویس در این است که در لایه فایروال، تست از داخل سرور با localhost موفق می‌شود، ولی تست از بیرون موفق نمی‌شود. برای تشخیص دقیق، این دو تست را جداگانه انجام دهید.

لایه شبکه

لایه سوم، شبکه است. اگر سرور در یک شبکه داخلی باشد و کلاینت از اینترنت به آن دسترسی داشته باشد، ممکن است مسیر شبکه بسته باشد. تفاوت این لایه با لایه فایروال در این است که در لایه شبکه، حتی فایروال محلی هم باز است ولی مسیریابی در سطح روتر مشکل دارد. ابزار traceroute و mtr در این مرحله به کار می‌آید.

در کنار این سه لایه، یک نکته عملی مهم وجود دارد: در پروژه‌های مبتنی بر کانتینر و ارکستریشن، لایه‌های سرویس و شبکه می‌توانند کاملاً متفاوت از آن‌چه انتظار دارید عمل کنند. برای درک دقیق‌تر این بافت، مرور «سرور چیست و چگونه کار می‌کند» و «هاست چیست و چگونه انتخاب کنیم» توصیه می‌شود؛ چون درک لایه‌بندی شبکه در این محیط‌ها، پیش‌نیاز تشخیص دقیق است.

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

در کنار ریشه‌های سمت سرور، بخش دیگری از این خطاها از سمت کلاینت و کد ناشی می‌شود. چهار ریشه اصلی که در تجربه من زیاد دیده‌ام، عبارتند از: آدرس اشتباه، پروتکل اشتباه، تنظیمات پروکسی اشتباه، و خطا در تشخیص خطا.

آدرس اشتباه

اولین ریشه، آدرس اشتباه در درخواست است. اگر به‌جای https://api.example.com به‌اشتباه https://api.example.com:8080 بنویسید، حتی اگر سرور اصلی فعال باشد، خطای REFUSED رخ می‌دهد. این تخلف، مخصوصاً در پروژه‌هایی که آدرس‌ها در متغیرهای محیطی تعریف می‌شوند، شایع است.

پروتکل اشتباه

دومین ریشه، پروتکل اشتباه است. اگر سرور مقصد فقط HTTPS را می‌پذیرد و شما درخواست HTTP می‌فرستید، بسته روی پورت ۸۰ می‌رود که در سرور باز نیست و خطای REFUSED رخ می‌دهد. راه‌حل استاندارد، تعریف صریح پروتکل در آدرس است.

تنظیمات پروکسی اشتباه

سومین ریشه، تنظیمات پروکسی اشتباه است. اگر مرورگر یا سیستم‌عامل شما برای درخواست‌های خاص، از یک پروکسی استفاده می‌کند که آن پروکسی در آن لحظه فعال نیست، خطای REFUSED رخ می‌دهد. این مسئله، مخصوصاً در محیط‌های سازمانی که پروکسی برای همه درخواست‌ها اجباری است، شایع است. برای بررسی، تنظیمات پروکسی مرورگر و سیستم را بررسی کنید.

خطا در تشخیص خطا

چهارمین ریشه، خطا در تشخیص خطا است. یعنی کد شما به‌درستی خطای شبکه را می‌گیرد ولی آن را با یک خطای دیگر اشتباه می‌گیرد. مثلاً TypeError: Failed to fetch را با خطای CORS یکی فرض می‌کند و سعی می‌کند CORS را برطرف کند، در حالی که ریشه در لایه TCP است. راه‌حل، تفکیک دقیق لایه خطا بر اساس تست‌های مستقل است.

در بافت درخواست‌های شبکه، یکی از پرتکرارترین موارد سردرگمی، اشتباه گرفتن این خطا با Unexpected token in JSON است. اگر پاسخ سرور HTML باشد و کد شما آن را به JSON پارس کند، خطای متفاوتی رخ می‌دهد که در نگاه اول شبیه خطای شبکه است. برای درک دقیق‌تر این تفاوت، مرور «خطای Unexpected token in JSON» توصیه می‌شود.

چطور این خطا را در پروژه ایزوله کنیم؟

فرض کنید همین امروز یک خطای ERR_CONNECTION_REFUSED در محیط تولید ظاهر شده و می‌خواهید ریشه‌اش را پیدا کنید. روشی که در این نوع پرونده‌ها به کار می‌گیرم، شش گام دارد و هر گام، یک شرط را در ذهن من حذف می‌کند.

گام اول: بررسی آدرس و پورت در کد

اولین کاری که می‌کنم، بررسی دقیق آدرس و پورت درخواست است. آیا آدرس درست است؟ آیا پورت درست است؟ آیا پروتکل درست است؟ این بررسی ساده، در تجربه من نیمی از پرونده‌ها را بسته است. توصیه می‌کنم آدرس کامل درخواست را در کنسول لاگ کنید:

const url = "https://api.example.com:443/data";
console.log("Requesting:", url);
const res = await fetch(url);

گام دوم: تست با curl

دومین کاری که می‌کنم، تست با curl است. این ابزار، مستقل از مرورگر عمل می‌کند و می‌تواند خطا را در سطح بالاتری نشان دهد:

curl -v https://api.example.com/data

اگر curl هم خطای «Connection refused» می‌دهد، مسئله در لایه سرویس مقصد است. اگر curl پاسخ می‌گیرد ولی مرورگر خطا می‌دهد، مسئله در سمت کلاینت یا CORS است.

گام سوم: تست از داخل سرور

سومین کاری که می‌کنم، تست از داخل سرور مقصد است. اگر به سرور دسترسی SSH دارید، از داخل سرور با curl http://localhost:port/health تست کنید. اگر این تست موفق شد ولی تست از بیرون ناموفق بود، مسئله در فایروال یا شبکه است.

گام چهارم: بررسی لاگ سرویس

چهارمین کاری که می‌کنم، بررسی لاگ سرویس مقصد است. اگر سرویس به‌تازگی کرش کرده یا خطایی ثبت کرده، در همان لاگ قابل مشاهده است. در پروژه‌های بزرگ که لاگ‌ها متمرکز هستند، ابزارهایی مثل ELK یا Loki در این مرحله به‌شکل چشمگیری مفید هستند.

گام پنجم: بررسی تنظیمات فایروال

پنجمین کاری که می‌کنم، بررسی تنظیمات فایروال است. در سرورهای لینوکسی، ابزارهای ufw status، iptables -L و firewall-cmd --list-all در این مرحله به کار می‌آید. اگر پورت مقصد در فایروال بسته است، باید باز شود.

گام ششم: بررسی وضعیت شبکه

ششمین کاری که می‌کنم، بررسی وضعیت شبکه است. ابزارهایی مثل traceroute، mtr و ping در این مرحله به کار می‌آید. اگر مسیر شبکه به سرور مقصد قطع است، باید با اپراتور شبکه یا سرویس‌دهنده هاست تماس بگیرید.

در کنار این شش گام، یک تکنیک عملی مهم وجود دارد: در بافت جاوااسکریپت، اگر خطای TypeError: Failed to fetch دریافت کردید ولی در کنسول ERR_CONNECTION_REFUSED دیدید، این تفاوت را به‌عنوان نشانه در نظر بگیرید که خطا از لایه شبکه است، نه از لایه منطق کد. برای مرور دقیق‌تر این تکنیک، مرور «ابزارهای اشکال‌زدایی جاوااسکریپت» توصیه می‌شود.

الگوهای رفع و پیشگیری در کد مدرن

بعد از تشخیص، نوبت به رفع و پیشگیری است. شش الگوی عملی در پروژه‌ها به کارم آمده که هر کدام، در بافت متفاوتی مناسب است.

الگوی اول: try/catch با تفکیک خطا

در بافت جاوااسکریپت، خطاهای شبکه به‌شکل TypeError با پیام Failed to fetch ظاهر می‌شوند. تفکیک این خطا از سایر TypeErrorها، یکی از پرتکرارترین نیازها است:

async function safeFetch(url) {
  try {
    const res = await fetch(url);
    return { ok: true, data: await res.json() };
  } catch (error) {
    if (error instanceof TypeError && /Failed to fetch/i.test(error.message)) {
      return { ok: false, kind: "network", error };
    }
    return { ok: false, kind: "other", error };
  }
}

این الگو، امکان تصمیم‌گیری دقیق در لایه‌های بالاتر را فراهم می‌کند؛ مثلاً می‌توانید فقط برای خطاهای شبکه، retry انجام دهید.

الگوی دوم: health check پیش از درخواست‌های مهم

در پروژه‌هایی که درخواست‌ها حیاتی هستند (مثل پرداخت)، می‌توانید پیش از درخواست اصلی، یک health check ساده بفرستید:

async function isServiceUp(baseUrl) {
  try {
    const res = await fetch(`${baseUrl}/health`, { method: "HEAD" });
    return res.ok;
  } catch {
    return false;
  }
}

این الگو، تجربه کاربری را بهتر می‌کند؛ چون به‌جای انتظار طولانی، سریع به کاربر پیام مناسب می‌دهد.

الگوی سوم: timeout برای همه درخواست‌ها

درخواست‌های بدون timeout، در شرایط قطعی شبکه، می‌توانند به‌شکل نامحدود منتظر بمانند. استفاده از AbortController در این بافت توصیه می‌شود:

async function fetchWithTimeout(url, ms = 8000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

در بافت Promise و async، این الگو بسیار رایج است. برای درک دقیق‌تر آن، مرور «Promise در جاوااسکریپت» و «async و await در جاوااسکریپت» توصیه می‌شود.

الگوی چهارم: retry با backoff نمایی

در بعضی سناریوها، خطای REFUSED موقتی است و با تلاش مجدد برطرف می‌شود. استفاده از backoff نمایی، فشار روی سرور را کم می‌کند:

async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, 2 ** i * 500));
    }
  }
}

الگوی پنجم: صفحه خطای کاربرپسند

در بافت تجربه کاربری، به‌جای نمایش پیام خام مرورگر، می‌توانید صفحه خطای خودتان را نمایش دهید:

function showNetworkErrorUI() {
  document.body.innerHTML = `
    

سرویس موقتاً در دسترس نیست

لطفاً چند لحظه بعد دوباره تلاش کنید.

`; }

این الگو، به‌ویژه در پروژه‌هایی که تجربه کاربری برایشان اولویت دارد، بسیار مفید است.

الگوی ششم: monitoring و alerting

در پروژه‌های بالغ، خطاهای شبکه در سمت سرور و کلاینت ثبت می‌شوند و در صورت افزایش نرخ خطا، هشدار به تیم فنی ارسال می‌شود. ابزارهایی مثل Sentry، LogRocket و Grafana در این بافت به کار می‌آیند. در انتخاب بین این شش الگو، هیچ‌کدام را نباید به‌عنوان نسخه «درست» در نظر گرفت؛ انتخاب، به بافت پروژه و اندازه تیم بستگی دارد. برای مرور جامع‌تر الگوهای مدیریت خطا، مطالعه «مدیریت خطا در جاوااسکریپت» توصیه می‌شود.

حذف کامل این خطا از سیستم، نتیجه ترکیب چند الگوی طراحی است، نه یک توصیه تک‌خطی.

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

در بررسی پرونده‌های این خطا، هفت اشتباه تکراری دیده‌ام که هر کدام، به‌جای رفع مشکل، آن را پیچیده‌تر می‌کند.

  • نسبت دادن خطا به CORS بدون تست curl. شایع‌ترین اشتباه. اگر curl هم خطا می‌دهد، مسئله CORS نیست و باید در لایه سرویس یا فایروال جستجو کنید.
  • استفاده از try/catch باز. اگر catch شما هر خطایی را بی‌سروصدا بلعیده باشد، در لایه‌های بعدی به‌شکل باگ‌های مبهم‌تر ظاهر می‌شود.
  • نادیده گرفتن وضعیت سرویس مقصد. اگر سرویس مقصد اصلاً اجرا نشده باشد، هر تغییری در کد کلاینت بی‌اثر است. همیشه ابتدا سرویس مقصد را بررسی کنید.
  • نادیده گرفتن فایروال سمت سرور. در بعضی هاست‌ها، پورت‌های غیراستاندارد به‌طور پیش‌فرض بسته هستند. اگر پورت‌تان را باز نکرده‌اید، خطای REFUSED اجتناب‌ناپذیر است.
  • مخلوط کردن پروتکل‌ها. اگر سرور فقط HTTPS را می‌پذیرد، درخواست HTTP به‌شکل REFUSED ظاهر می‌شود. همیشه مطمئن شوید که پروتکل و پورت با یکدیگر سازگار هستند.
  • نادیده گرفتن تنظیمات پروکسی. در محیط‌های سازمانی، پروکسی می‌تواند منبع خطا باشد. اگر پروکسی در آن لحظه فعال نیست، خطای REFUSED رخ می‌دهد.
  • عدم مستندسازی پورت‌ها. اگر تیم فنی نداند که هر سرویس روی چه پورتی گوش می‌دهد، به‌سرعت فرض‌های اشتباه شکل می‌گیرد. مستندسازی مدل شبکه، در بلندمدت از ده‌ها ساعت دیباگ جلوگیری می‌کند.

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

پیامدهای امنیتی و سوءاستفاده از این خطا

خطاهای شبکه، در نگاه اول مسئله امنیتی به نظر نمی‌رسند، ولی در عمل، سه مسیر مهم برای سوءاستفاده از آن‌ها وجود دارد که در پروژه‌های واقعی دیده‌ام.

حملات اسکن شبکه داخلی

یکی از پرتکرارترین موارد سوءاستفاده، اسکن شبکه داخلی از طریق مرورگر کاربر است. اگر کد جاوااسکریپت بتواند بین «پورت باز» و «پورت بسته» تفکیک کند، مهاجم می‌تواند از این تفاوت برای کشف سرویس‌های داخلی کاربران استفاده کند. به همین دلیل، مرورگرهای مدرن، خطاهای شبکه‌ای مختلف را به‌طور یکسان نمایش می‌دهند. ولی در بعضی سناریوها، تفاوت‌های جزئی مثل زمان پاسخ یا وجود یا نبود خطای ERR_CONNECTION_REFUSED می‌تواند به‌عنوان سیگنال استفاده شود. راه‌حل استاندارد، اجرای timeout یکسان برای همه درخواست‌ها در سمت کلاینت و سرور است.

افشای اطلاعات از طریق پیام‌های خطا

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

try {
  return await fetch(url);
} catch (error) {
  throw new Error("Service temporarily unavailable");
}

حمله denial of service از طریق retry

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

const backoff = (attempt) => Math.min(2 ** attempt * 500, 30_000);

یک نکته کاربردی: هر بار که در جلسات فنی بحث روی نمایش پیام خطا به کاربر پیش می‌آید، یادآوری کنید که خطاهای شبکه یکی از پرخطرترین موارد برای افشای جزئیات داخلی هستند. توجه به این نکته در بازبینی کد، از بسیاری از نشت‌های اطلاعاتی جلوگیری می‌کند. در طراحی APIهای مدرن، استانداردهای امنیتی و راهنمای ساخت REST API می‌تواند به شما کمک کند تا این لایه را به‌شکل درست پیاده کنید؛ مرور «آموزش rest api» در این زمینه توصیه می‌شود.

ماتریس تست برای سناریوهای اتصال

چیزی که در پروژه‌های بالغ به‌شکل منظم دیده‌ام، تست‌های اختصاصی برای سناریوهای اتصال شبکه است. ماتریسی که در پروژه‌ها استفاده می‌کنم، این شکلی است:

سناریوورودیخروجی مورد انتظار
سرور فعال، پورت بازآدرس معتبرپاسخ موفق
سرور فعال، پورت اشتباهپورت نامعتبرERR_CONNECTION_REFUSED
سرور خاموشآدرس معتبرERR_CONNECTION_REFUSED یا TIMED_OUT
فایروال پورت را بستهآدرس معتبرERR_CONNECTION_TIMED_OUT
DNS نامعتبردامنه ناموجودERR_NAME_NOT_RESOLVED
پروتکل اشتباه (HTTP به‌جای HTTPS)http://domain:80ERR_CONNECTION_REFUSED
گواهی SSL نامعتبرhttps://domainERR_CERT_DATE_INVALID
پاسخ HTML به‌جای JSONآدرس اشتباهUnexpected token in JSON
timeout عدم پاسخسرور کندAbortError

هر ردیف از این ماتریس، یک سناریوی واقعی را پوشش می‌دهد. اگر این تست‌ها را در پروژه خود بگذارید، نه‌فقط خطاهای زمان اجرا را زودتر می‌گیرید، بلکه وقتی تیم شما بزرگ‌تر می‌شود، رفتار برنامه در برابر تغییرات، قابل پیش‌بینی می‌ماند.

یک تذکر مهم: در تست‌های async، مطمئن شوید که هر تست، به‌شکل دقیق، خطا را در همان لایه‌ای که انتظار دارید دریافت می‌کند. بعضی از فریم‌ورک‌های تست، خطاها را در لایه‌ای بالاتر می‌گیرند و اگر شما به آن توجه نکنید، تست‌های موفق می‌سازید که در محیط واقعی شکست می‌خورند.

پرسش‌های پرتکرار درباره ERR_CONNECTION_REFUSED

در این بخش، پرسش‌هایی که بیشترین تکرار را در تیم‌های فنی، تیکت‌های پشتیبانی و جلسات بازبینی داشته‌اند، پاسخ می‌دهم. هدف این است که بتوانید به‌سرعت به پاسخ برسید، بدون اینکه لازم باشد کل مقاله را دوباره مرور کنید.

تفاوت این خطا با ERR_CONNECTION_TIMED_OUT چیست؟

اولی یعنی سرور فعال است ولی در آن پورت گوش نمی‌دهد. دومی یعنی سرور به درخواست شما پاسخ نداد و کلاینت در حالت انتظار ماند. اولی نشانه دقیق‌تری است: شبکه سالم است، فقط سرویس مقصد مشکل دارد. برای بررسی، از telnet استفاده کنید؛ اگر بلافاصله «Connection refused» دیدید، اولی است.

آیا این خطا از سمت جاوااسکریپت قابل تشخیص است؟

در بافت جاوااسکریپت، این خطا به‌شکل TypeError: Failed to fetch ظاهر می‌شود. تفکیک دقیق این خطا از سایر TypeErrorها با استفاده از error instanceof TypeError و بررسی message امکان‌پذیر است. برای درک دقیق‌تر این رفتار، مرور «خطای TypeError در جاوااسکریپت» توصیه می‌شود.

آیا این خطا در همه مرورگرها یکسان است؟

پیام دقیق در مرورگرهای مبتنی بر Chromium (Chrome، Edge) به‌شکل net::ERR_CONNECTION_REFUSED است. در Firefox، پیام متفاوتی مثل NS_ERROR_CONNECTION_REFUSED ظاهر می‌شود. در Safari، پیام ممکن است کاملاً متفاوت باشد. به همین دلیل، روی متن دقیق پیام تکیه نکنید؛ روی موقعیت لایه‌ای (لایه TCP) تکیه کنید.

چطور بفهمم ریشه از سمت سرور است یا از سمت کلاینت؟

بهترین راه، تست با curl از یک سیستم مستقل است. اگر curl هم خطا می‌دهد، ریشه از سمت سرور است. اگر curl پاسخ می‌گیرد ولی مرورگر خطا می‌دهد، ریشه از سمت کلاینت یا CORS است.

آیا timeout می‌تواند این خطا را برطرف کند؟

خیر. این خطا در زمان بسیار کوتاه (چند میلی‌ثانیه) رخ می‌دهد چون سرور با RST پاسخ می‌دهد. timeout فقط برای خطاهای TIMED_OUT یا RESET مفید است.

آیا در Docker این خطا شایع است؟

بله. اگر کلاینت در یک container و سرور در container دیگری باشد، پورت‌ها در شبکه داخلی Docker با پورت‌های هاست تفاوت دارند. راه‌حل، استفاده از نام container در شبکه داخلی یا انتشار صریح پورت در تنظیمات Docker است.

آیا در HTTPS هم این خطا رخ می‌دهد؟

بله. در HTTPS، اگر پورت ۴۴۳ بسته باشد یا سرویس SSL گوش ندهد، همین خطا رخ می‌دهد. تفاوت این دو فقط در لایه‌ای است که خطا در آن رخ می‌دهد. اگر خطای REFUSED در HTTPS دیدید، احتمالاً سرویس SSL روی آن پورت فعال نیست.

آیا این خطا با CDN برطرف می‌شود؟

در بعضی سناریوها، بله. اگر CDN درخواست‌ها را در لبه پاسخ دهد، فشار از سرویس مقصد برداشته می‌شود. ولی اگر ریشه در دسترسی سرویس مقصد به اینترنت باشد، CDN هم نمی‌تواند کمکی کند.

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

در بعضی سناریوها، بله. اگر ریشه در محدودیت شبکه محلی باشد، پروکسی می‌تواند از آن عبور کند. ولی اگر ریشه در سمت سرویس مقصد باشد، پروکسی بی‌اثر است و حتی ممکن است خطا را پیچیده‌تر کند.

نگاه معمارانه: اتصال به‌عنوان یک قرارداد

در پروژه‌های بالغ، اتصال شبکه به‌عنوان یک تصمیم طراحی مدیریت می‌شود، نه به‌عنوان یک اتفاق تصادفی. سه تصمیم معمارانه که در تیم‌های حرفه‌ای دیده‌ام، این کلاس خطا را از یک خطر پنهان به یک ابزار قابل کنترل تبدیل می‌کند.

لایه اول: قرارداد صریح برای سرویس‌ها

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

لایه دوم: health check و circuit breaker

در پروژه‌های توزیع‌شده، به‌جای انتظار برای خطا در زمان درخواست واقعی، از health check و circuit breaker استفاده می‌شود. یعنی پیش از هر درخواست مهم، وضعیت سرویس مقصد بررسی می‌شود و در صورت عدم دسترسی، درخواست به‌شکل سریع رد می‌شود، نه با تأخیر و خطای مبهم. این الگو، تجربه کاربری را بهبود می‌بخشد و فشار روی سرویس‌های معیوب را کم می‌کند. برای درک دقیق‌تر این الگو در بافت معماری سرویس‌ها، مرور «معماری وب چیست» توصیه می‌شود.

لایه سوم: نوع‌های متمایز برای خطاهای شبکه و منطق

در پروژه‌های TypeScript، می‌توانید دو تایپ جداگانه تعریف کنید: یکی برای «خطاهای شبکه‌ای که با retry قابل رفع هستند» و یکی برای «خطاهای منطقی که با retry بدتر می‌شوند». این تمایز، در زمان کامپایل، جلوی بسیاری از خطاهای رفتاری را می‌گیرد و بازبینی کد را ساده‌تر می‌کند. تجربه من این است که این تغییر کوچک، در طول یک سال، تعداد خطاهای شبکه‌ای را به‌شکل محسوسی کاهش می‌دهد.

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

وقتی اتصال شبکه به‌عنوان قرارداد دیده شود، از یک اتفاق تصادفی به یک تعهد مشخص تبدیل می‌شود.

یک عادت کوچک، یک کلاس خطای قابل پیش‌بینی

خطای net::ERR_CONNECTION_REFUSED در نگاه اول یک خطای کوچک به‌نظر می‌رسد، اما در عمل، آینه‌ای است که نشان می‌دهد لایه شبکه پروژه شما چقدر صریح و کنترل‌شده است. اگر این خطا در تولید ظاهر می‌شود، به احتمال زیاد جای دیگری از سیستم هم فرض‌های ضمنی درباره وضعیت سرویس‌ها دارد. به همین دلیل، توصیه عملی من سه چیز است: اول، همیشه پیش از هر اقدامی با curl تست کنید تا لایه خطا مشخص شود؛ دوم، در مرزهای سیستم، یک لایه health check متمرکز بگذارید؛ سوم، در تست‌های خود ماتریس سناریوهای اتصال را بگنجانید تا رفتار برنامه در برابر تغییرات ناخواسته، قابل پیش‌بینی بماند.

اگر خطای مشابهی را در یک پروژه واقعی تجربه کرده‌اید — مخصوصاً جایی که ریشه مشکل از آنچه انتظار داشتید دور بوده — تجربه‌تان را در دیدگاه بنویسید. برای من جالب است بدانم کدام لایه بیشترین زمان را از شما گرفت، و آیا الگویی پیدا کردید که با آنچه در این متن آمده، تفاوت داشت. تجربه‌های واقعی شما، این متن را برای خواننده بعدی دقیق‌تر می‌کند. 🌐