تخطَّ إلى المحتوى

٢١ يوليو ٢٠٢٦

حل خطأ Access to fetch has been blocked by CORS policy بالعربي

شارك المقال:

كل من ربط واجهة أمامية بخادم 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) تتولّى هذا تلقائيًّا إن ضبطتها بشكل صحيح، بدل كتابة الترويسات يدويًّا لكل مسار.

خلاصة التشخيص السريع

  1. اقرأ الرسالة: هل تذكر غياب الترويسة، أم تعارض * مع Allow-Credentials؟ كل سبب حلّه مختلف قليلًا.
  2. تحقّق من تبويب Network في أدوات المطوّر: هل صدر طلب OPTIONS منفصل؟ هل ردّ عليه الخادم بترويسات CORS؟
  3. أضف نطاقك لقائمة بيضاء صريحة على الخادم — لا حلولًا مؤقّتة بجهة المتصفّح (إضافات تعطيل CORS) قد تعمل محليًّا لكنها لا تحلّ شيئًا بالإنتاج.

تعمّق أكثر بمفهوم CORS وإساءة إعداده ضمن مسار الأمن السيبراني الكامل بالعربي.

📚 مصادر رسمية للتعمّق: توثيق HTTP والشبكات على MDN

الأسئلة الشائعة

لماذا يعمل الطلب من Postman لكن يفشل من المتصفّح بنفس الرابط بالضبط؟

لأن سياسة CORS قيد يفرضه المتصفّح فقط — أدوات مثل Postman أو curl لا تطبّقها إطلاقًا لأنها ليست صفحة ويب معرّضة لخطر الطلبات الخبيثة عبر النطاقات. الخادم نفسه يستجيب بنفس البيانات في الحالتين؛ الفرق فقط في هل يفحص أحد الطرفين الترويسات قبل السماح بقراءة الاستجابة.

هل حل المشكلة بإضافة Access-Control-Allow-Origin: * آمن؟

آمن فقط لموارد عامة لا تتطلّب مصادقة (بيانات علنية لا تخصّ مستخدمًا معيّنًا). أما لو كان الطلب يحمل كوكيز الجلسة أو رمز Authorization، فالمتصفّح يرفض هذا المزيج فعليًّا (WildCard مع Allow-Credentials) — والحل الصحيح تحديد النطاق المسموح صراحة بدل النجمة، مع التحقّق منه مقابل قائمة بيضاء لا انعكاسه أعمى.

أضفت الترويسة لكن الخطأ لا يزال يظهر — لماذا؟

تحقّق أولًا من طلب OPTIONS (الفحص المسبق / preflight) — المتصفّح يرسله تلقائيًّا قبل طلبات معيّنة (مثل PUT أو طلبات فيها ترويسات مخصّصة)، ويتوقّع أن يردّ الخادم عليه أيضًا بترويسات CORS كاملة، لا فقط على الطلب الفعلي بعده. كثير من الأخطاء المستمرّة سببها خادم يضيف الترويسة على GET/POST فقط وينسى مسار OPTIONS.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.