خطای CORS را کامل بفهمید و برای همیشه حلش کنید

خطای CORS را کامل بفهمید و برای همیشه حلش کنید
در این مقاله می‌خوانید
  1. CORS دقیقاً چیست؟
  2. چرا فقط جاوااسکریپت؟
  3. درخواست پیش‌پرواز (Preflight)
  4. راه‌حل: در سرور
  5. وقتی کوکی می‌فرستید
  6. تنظیم عملی در سرورهای رایج
  7. Express
  8. Nginx
  9. لاراول
  10. وقتی چند زیردامنه دارید
  11. اشتباهات رایج
  12. چرا در محیط توسعه پیش نمی‌آید
  13. پیدا کردنش در production
  14. جمع‌بندی
  15. منابع و مطالعهٔ بیشتر

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

Access to fetch at 'https://api.example.com/data' from origin
'https://example.com' has been blocked by CORS policy

اولین واکنش تقریباً همه این است که دنبال راهی برای «خاموش کردن CORS» بگردند. این‌جا اولین سوءتفاهم شکل می‌گیرد — چون CORS چیزی نیست که شما روشنش کرده باشید تا بتوانید خاموشش کنید.

CORS دقیقاً چیست؟

مرورگرها قاعده‌ای قدیمی به نام سیاست هم‌ریشه دارند: جاوااسکریپتی که در صفحهٔ a.com اجرا می‌شود، به‌طور پیش‌فرض نمی‌تواند پاسخ درخواستی به b.com را بخواند.

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

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

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

چرا فقط جاوااسکریپت؟

نکته‌ای که سردرگمی زیادی می‌سازد: تگ <img> از هر دامنه‌ای تصویر بارگذاری می‌کند و مشکلی ندارد. فرم HTML به هر دامنه‌ای ارسال می‌شود. حتی درخواست شما با curl هم بی‌مشکل جواب می‌گیرد.

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

درخواست پیش‌پرواز (Preflight)

بخشی که بیشترین سردرگمی را ایجاد می‌کند. مرورگر درخواست‌ها را به دو دسته تقسیم می‌کند.

درخواست‌های ساده مستقیم فرستاده می‌شوند: متد GET، HEAD یا POST با هدرهای متعارف.

بقیه پیش از ارسال، یک درخواست OPTIONS می‌فرستند تا اجازه بگیرند. این حالت وقتی پیش می‌آید که متد PUT، PATCH یا DELETE باشد، یا هدر سفارشی داشته باشید — مثل Authorization یا Content-Type: application/json.

و همین‌جاست که دام کلاسیک قرار دارد: ارسال JSON با توکن احراز هویت، همیشه preflight ایجاد می‌کند. بسیاری از تیم‌ها هدرهای CORS را روی مسیر اصلی تنظیم می‌کنند ولی سرورشان به OPTIONS پاسخ درستی نمی‌دهد؛ نتیجه این می‌شود که GET کار می‌کند و POST نه.

راه‌حل: در سرور

سرور باید هدرهای درست را برگرداند. حداقل‌ها:

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

و باید به درخواست OPTIONS با وضعیت ۲۰۰ یا ۲۰۴ پاسخ دهد، پیش از آنکه به لایهٔ احراز هویت برسد. اگر میان‌افزار احراز هویت شما OPTIONS را هم بررسی کند، آن را با ۴۰۱ رد می‌کند و مرورگر خطای CORS نشان می‌دهد — در حالی که مشکل اصلاً CORS نیست.

وقتی کوکی می‌فرستید

اگر احراز هویت شما مبتنی بر کوکی است، دو تغییر لازم است. در فرانت‌اند:

fetch(url, { credentials: 'include' })

و در سرور:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com

در این حالت * مجاز نیست. باید دامنهٔ دقیق را بنویسید؛ این محدودیت عمدی است، چون در غیر این صورت هر سایتی می‌توانست با کوکی‌های کاربر درخواست بزند.

تنظیم عملی در سرورهای رایج

Express

const cors = require('cors');
app.use(cors({
  origin: ['https://example.com', 'https://www.example.com'],
  credentials: true,
}));

این میان‌افزار باید پیش از مسیرها و پیش از میان‌افزار احراز هویت قرار بگیرد، وگرنه درخواست OPTIONS به آن نمی‌رسد.

Nginx

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin  "https://example.com";
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
        add_header Access-Control-Max-Age 86400;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://example.com" always;
    proxy_pass http://backend;
}

کلمهٔ always مهم است: بدون آن، هدر روی پاسخ‌های خطا (۴xx و ۵xx) اضافه نمی‌شود و شما خطای CORS می‌گیرید در حالی که مشکل چیز دیگری است.

لاراول

لاراول از نسخه‌های اخیر پیکربندی CORS را در config/cors.php دارد؛ کافی است allowed_origins را تنظیم کنید و مطمئن شوید مسیرهای API در paths آمده‌اند.

وقتی چند زیردامنه دارید

هدر Access-Control-Allow-Origin فقط یک مقدار می‌پذیرد؛ فهرست دادن کار نمی‌کند. الگوی درست این است که مبدأ درخواست را بخوانید، با فهرست مجاز خودتان بسنجید، و اگر مجاز بود همان را برگردانید:

const allowed = ['https://example.com', 'https://app.example.com'];
const origin = req.headers.origin;
if (allowed.includes(origin)) res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');   // تا کش، پاسخ یک مبدأ را به مبدأ دیگر ندهد

هدر Vary: Origin را فراموش نکنید. بدون آن، CDN یا کش مرورگر می‌تواند پاسخی که برای یک مبدأ ساخته شده را به مبدأ دیگری بدهد و باگ‌های بسیار گیج‌کننده‌ای بسازد.

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

گذاشتن * در production. برای API عمومی مشکلی ندارد، ولی برای APIای که داده‌های کاربر را برمی‌گرداند یعنی هر سایتی می‌تواند از مرورگر کاربران شما به آن درخواست بزند.

اسلش اضافه. https://example.com/ با https://example.com برابر نیست. مرورگر تطبیق را دقیق انجام می‌دهد.

فراموش کردن پورت یا پروتکل. http://localhost:3000 و http://localhost:5173 دو مبدأ متفاوت‌اند.

افزونه‌های «رفع CORS» در مرورگر. برای دیدن سریع یک پاسخ خوب‌اند، ولی مشکل را حل نمی‌کنند — فقط برای شما پنهانش می‌کنند. کاربران شما آن افزونه را ندارند.

خطای CORS که اصلاً CORS نیست. اگر سرور خطای ۵۰۰ بدهد و در مسیر خطا هدرهای CORS را نگذارد، مرورگر خطای CORS نشان می‌دهد. توسعه‌دهنده ساعت‌ها دنبال تنظیمات CORS می‌گردد در حالی که مشکل یک باگ سمت سرور است. همیشه اول تب Network را ببینید: اگر وضعیت پاسخ ۵۰۰ است، مشکل شما CORS نیست.

چرا در محیط توسعه پیش نمی‌آید

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

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

پیدا کردنش در production

خطاهای CORS معمولاً برای همهٔ کاربران رخ نمی‌دهند؛ فقط برای بخشی که مسیر خاصی را طی می‌کنند یا از زیردامنهٔ دیگری وارد شده‌اند. بدون سیستم ثبت خطا، این‌ها هرگز دیده نمی‌شوند — چون درخواست ناموفق است، کاربر فقط می‌بیند «چیزی بارگذاری نشد» و معمولاً گزارشی هم نمی‌دهد.

اگر رصد خودکار خطای شبکه داشته باشید، این دسته با الگوی مشخصی خودشان را نشان می‌دهند: همهٔ متأثران یک مبدأ مشترک دارند که در فهرست مجاز سرور نیست. معمولاً www است که یادتان رفته اضافه کنید.

جمع‌بندی

CORS محافظ است، نه مانع. اگر خطایش را می‌بینید، یعنی مرورگر دارد کار درستش را انجام می‌دهد و سرور شما هنوز نگفته که این مبدأ مجاز است.

مسیر رفع همیشه یکی است: مبدأ دقیق را در سرور مجاز کنید، مطمئن شوید OPTIONS پیش از احراز هویت پاسخ می‌گیرد، و اگر کوکی می‌فرستید دامنه را صریح بنویسید. و پیش از هر کاری، تب Network را ببینید تا مطمئن شوید واقعاً با CORS طرفید نه با خطای ۵۰۰ی که لباس CORS پوشیده است. خطاهای رایج دیگر مرورگر را در راهنمای جامع خطاهای جاوااسکریپت مرور کرده‌ایم.

منابع و مطالعهٔ بیشتر

باگ‌ماگ را رایگان امتحان کنید

خطاهای سایتتان را خودکار ثبت کنید و بگذارید کاربران با یک کلیک باگ گزارش دهند.

شروع رایگان