كل من ربط واجهة أمامية بخادم API على نطاق مختلف (حتى لو منفذ مختلف فقط، مثل localhost:3000 مقابل localhost:5000) صادف هذه الرسالة في console المتصفّح:
Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the
requested resource.
الخبر الجيد: هذه الرسالة واضحة جدًّا بمجرّد فهم من يصدرها ولماذا.
من يمنع الطلب فعليًّا؟
المتصفّح، لا خادمك. الطلب فعليًّا وصل للخادم ونفّذه وأعاد استجابة كاملة — لكن المتصفّح يحجب قراءتك لتلك الاستجابة داخل كودك، لأن الخادم لم يُصرّح صراحة أن نطاقك مسموح له بذلك.
هذا سلوك مقصود يسمّى سياسة المصدر الواحد (Same-Origin Policy): افتراضيًّا، صفحة من نطاق app.example.com لا يمكنها قراءة استجابة طلب لنطاق api.example.com مختلف، تحديدًا لمنع موقع خبيث من قراءة بيانات حسّاسة بالنيابة عن مستخدم زار صفحته وهو مسجّل دخوله بموقع آخر.
CORS (Cross-Origin Resource Sharing) هي الآلية التي يستخدمها الخادم لفتح استثناء محسوب لهذه السياسة — عبر ترويسة استجابة صريحة.
الحل الصحيح: من إعدادات الخادم فقط
لا يوجد إعداد بجهة الواجهة الأمامية يحلّ هذا فعليًّا — لأن المتصفّح يفحص استجابة الخادم، لا كود fetch أو axios لديك. أضف الترويسة على الخادم:
// مثال Express.js: اسمح لنطاق واجهتك الأمامية تحديدًا
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "https://app.example.com");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
next();
});
لو تدير أكثر من نطاق (بيئة تطوير محلّية + إنتاج)، تحقّق من ترويسة Origin الواردة مقابل قائمة بيضاء بدل تثبيت نطاق واحد:
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"http://localhost:3000",
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
next();
});
لماذا لا تستخدم * ببساطة وتنتهي المشكلة؟
Access-Control-Allow-Origin: * تسمح لأي نطاق بقراءة الاستجابة — تعمل فورًا وتزيل الخطأ، لكنها خطر أمني حقيقي لو كانت البيانات المُرجَعة خاصّة بمستخدم مسجّل دخول (تحمل كوكيز أو رمز مصادقة). المتصفّح نفسه يرفض هذا المزيج تحديدًا:
⚠️ لا يمكن الجمع بين
Access-Control-Allow-Origin: *وAccess-Control-Allow-Credentials: true— المتصفّح يحجب الطلب حتى لو حاولت. لو تعتمد على كوكيز الجلسة، يجب تحديد نطاق صريح بدل النجمة.
استخدم * فقط لموارد عامة فعلًا لا تتطلّب مصادقة (بيانات علنية، ملفّات ثابتة عامة). لأي شيء يخصّ مستخدمًا معيّنًا، القائمة البيضاء الصريحة هي الحلّ الوحيد الآمن.
طلب OPTIONS: السبب الأشهر لبقاء الخطأ رغم إضافة الترويسة
لطلبات معيّنة (مثل PUT/DELETE، أو أي طلب يحمل ترويسة مخصّصة كـ Authorization)، يرسل المتصفّح تلقائيًّا طلب فحص مسبق (preflight) بصيغة OPTIONS قبل الطلب الفعلي، ليتأكّد أن الخادم يسمح بهذا النوع من الطلبات أصلًا. لو ردّ خادمك على OPTIONS بخطأ 404 أو بلا ترويسات CORS (لأن كودك يضيفها فقط على مسارات GET/POST العادية)، يفشل الطلب كاملًا قبل حتى أن يصل لمنطق التطبيق الفعلي.
تأكّد أن خادمك يردّ على OPTIONS بترويسات CORS كاملة أيضًا — أغلب أطر العمل الحديثة (Express عبر حزمة cors، Django عبر django-cors-headers) تتولّى هذا تلقائيًّا إن ضبطتها بشكل صحيح، بدل كتابة الترويسات يدويًّا لكل مسار.
خلاصة التشخيص السريع
- اقرأ الرسالة: هل تذكر غياب الترويسة، أم تعارض
*معAllow-Credentials؟ كل سبب حلّه مختلف قليلًا. - تحقّق من تبويب Network في أدوات المطوّر: هل صدر طلب
OPTIONSمنفصل؟ هل ردّ عليه الخادم بترويسات CORS؟ - أضف نطاقك لقائمة بيضاء صريحة على الخادم — لا حلولًا مؤقّتة بجهة المتصفّح (إضافات تعطيل CORS) قد تعمل محليًّا لكنها لا تحلّ شيئًا بالإنتاج.
تعمّق أكثر بمفهوم CORS وإساءة إعداده ضمن مسار الأمن السيبراني الكامل بالعربي.