خطأ CORS ("No 'Access-Control-Allow-Origin' header is present") يعني أن صفحة ويب على مصدر ما، مثل http://localhost:5173، استدعت واجهة API الخاصة بك على مصدر آخر، فرفض المتصفح أن يدع الصفحة تقرأ الاستجابة لأن الاستجابة لم تقل إن ذلك المصدر مسموح. الإصلاح على الخادم: أرسل Access-Control-Allow-Origin بمصدر الصفحة، وأجب على طلب OPTIONS التمهيدي (preflight) الذي يرسله المتصفح بالطرق والترويسات التي تقبلها. في Express هذا بضعة أسطر من الـ middleware، أو app.use(cors()) من حزمة cors.
الاستجابة هي قائمة المنتجات، ويعرض تبويب Headers فيها access-control-allow-origin: https://app.example.com: يحق لصفحة على ذلك المصدر قراءتها. الآن أرسل OPTIONS /api/products/1 مع الترويستين Origin: https://app.example.com وAccess-Control-Request-Method: PUT. هذا هو الطلب التمهيدي الذي يرسله المتصفح قبل PUT، والإجابة 204 مع الطرق والترويسات المسموحة ودون جسم.
ما الذي يفحصه المتصفح
الصفحات من المصدر نفسه لا تحتاج إلى شيء. أما في الطلب عبر المصادر فيقرأ المتصفح ترويسات الاستجابة هذه:
| الترويسة | المعنى |
|---|---|
Access-Control-Allow-Origin | المصدر الوحيد المسموح له بقراءة الاستجابة، أو * لأي مصدر |
Access-Control-Allow-Methods | الطرق المسموحة في الطلب الحقيقي (في الطلب التمهيدي فقط) |
Access-Control-Allow-Headers | ترويسات الطلب التي يحق للصفحة إرسالها، مثل Authorization (في الطلب التمهيدي فقط) |
Access-Control-Allow-Credentials | true إن كان يحق للصفحة إرسال ملفات cookie وقراءة الإجابة |
Access-Control-Max-Age | عدد الثواني التي يحق للمتصفح فيها تخزين إجابة الطلب التمهيدي |
Vary: Origin | يخبر الذاكرات المؤقتة أن الإجابة تعتمد على ترويسة الطلب Origin |
طلب GET بسيط أو POST على نمط النماذج يذهب مباشرة إلى الخادم، ويفحص المتصفح Access-Control-Allow-Origin في الإجابة. يبقى الخادم ينفّذ الطلب؛ وCORS تقرر فقط هل ترى الصفحة النتيجة. أما الطلب الذي فيه جسم JSON، أو ترويسة Authorization، أو طريقة مثل PUT أو DELETE، فيسبقه طلب OPTIONS تمهيدي، ولا يُرسل الطلب الحقيقي إلا إذا سمحت إجابة الطلب التمهيدي بذلك. لهذا يجب أن يجيب middleware الخاص بـ CORS على OPTIONS قبل أن يعمل أي مسار أو فحص مصادقة.
السماح بعدة مصادر، مع بيانات الاعتماد
تحمل Access-Control-Allow-Origin مصدرًا واحدًا أو *، لا قائمة أبدًا. للسماح بعدة مصادر، قارن ترويسة Origin في الطلب بقائمة مسموحة وأعِدها كما هي حين تتطابق:
لا يحمل الطلب الأول ترويسة Origin، مثل استدعاء من curl أو من صفحة على المصدر نفسه، فيحصل على {"origin":null} دون ترويسات CORS. أرسل GET /api/me مع الترويسة Origin: http://localhost:5173 فيعرض تبويب Headers ذلك المصدر معادًا إضافة إلى access-control-allow-credentials: true. غيّر الترويسة إلى Origin: https://evil.example فتختفي ترويستا CORS كلتاهما: ما زال الخادم يجيب، لكن المتصفح سيمنع الصفحة من قراءة الإجابة. تسمح Access-Control-Allow-Credentials: true للصفحة بإرسال ملفات cookie (عبر fetch(url, { credentials: 'include' }))، وترفضها المتصفحات إذا اجتمعت مع *، فبيانات الاعتماد تحتاج دائمًا إلى المصدر الدقيق.
حزمة cors
تؤدي حزمة cors العمل نفسه عبر خيارات. ثبّتها بـ npm install cors:
const cors = require('cors');
// Any origin, no cookies: fine for a public read-only API.
app.use(cors());
// Only your frontend, with cookies.
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// One route only.
app.get('/api/public', cors(), handler);
عند استدعائها دون خيارات، ترسل cors() الترويسة Access-Control-Allow-Origin: * وتجيب على كل طلب تمهيدي. لا تستخدم * إلا للبيانات العامة التي لا تحتاج إلى تسجيل دخول.
أخطاء شائعة
إضافة CORS بعد المسارات أو بعد middleware المصادقة. لا يحمل طلب OPTIONS التمهيدي ترويسة Authorization، فيجيبه فحص المصادقة الموضوع أولًا بـ 401 ويبلّغ المتصفح عن خطأ CORS. سجّل middleware الخاص بـ CORS أولًا.
ضبط * مع بيانات الاعتماد. ترفض المتصفحات الاستجابة. أعِد المصدر المحدد بدلًا من ذلك.
محاولة حل CORS في الواجهة الأمامية. خيارات الطلب مثل mode: 'no-cors' لا ترفع القيد؛ بل تعطيك استجابة معتمة لا تستطيع قراءتها. وحدها ترويسات الخادم تحل المشكلة.
لمعرفة كيف يعمل app.use() وترتيب الـ middleware، راجع Middleware في Express.
الأسئلة الشائعة
لماذا تعمل واجهة API في Postman أو curl ولا تعمل في المتصفح؟
لا تفرض CORS إلا المتصفحات. يتجاهل Postman وcurl والخوادم الأخرى ترويسات CORS، فيقرؤون الاستجابة في كل الأحوال. أما المتصفح فيرفض تسليم الاستجابة لسكربتات صفحة من مصدر آخر ما لم يسمح الخادم بذلك المصدر.
هل CORS ميزة أمان لخادمي؟
إنها تحمي المستخدمين لا خادمك. تمنع صفحة خبيثة من قراءة استجابات واجهة API الخاصة بك بملفات cookie الخاصة بالمستخدم. وما زال بإمكان أي أحد استدعاء واجهتك من سكربت أو خادم، فالمصادقة والتفويض ما زالا مسؤوليتك.
ما الذي يُعد مصدرًا مختلفًا؟
المصدر (origin) هو البروتوكول والمضيف والمنفذ معًا. http://localhost:5173 وhttp://localhost:3000 مصدران مختلفان لأن المنفذين مختلفان، وhttps://example.com وhttps://api.example.com يختلفان في المضيف.
هل يمكنني تجنب CORS أثناء التطوير؟
نعم، بجعل الطلب من المصدر نفسه: اضبط خادم تطوير الواجهة الأمامية (server.proxy في Vite مثلًا) ليحوّل /api إلى خادم Express. عندها يتحدث المتصفح مع مصدر واحد ولا حاجة إلى ترويسات CORS.