CORS للمبتدئ: ليه المتصفح بيرفض طلب الـ API بتاعك وإزاي تصلّحه صح
لو الـ fetch بيطلعلك خطأ أحمر في الكونسول فيه كلمة CORS، ركّز في نقطة واحدة: المشكلة مش في كودك، ومش في السيرفر بيقع. الطلب بيوصل السيرفر فعلاً، والسيرفر بيرد عادي. اللي بيحصل إن المتصفح نفسه بيرفض إنه يسلّمك الرد، لأنه بيحميك. في المقال ده هتفهم ليه بيعمل كده، وتصلّحها صح بترويسة واحدة من السيرفر.
المشكلة باختصار
عندك فرونت إند شغّال على https://app.example.com، وبيكلّم API على https://api.example.com. تفتح الكونسول تلاقي رسالة زي دي:
Access to fetch at 'https://api.example.com/orders' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.الرسالة واضحة أكتر مما تفتكر: السيرفر ما بعتش ترويسة اسمها Access-Control-Allow-Origin، فالمتصفح قرر يمنعك من قراءة الرد. الافتراض هنا إن الفرونت والـ API على أصلين مختلفين (origins مختلفة). لو الاتنين على نفس النطاق، المشكلة دي مش بتظهر أصلاً.
الفكرة بمثال: حارس مبنى الشركة
تخيّل شركة في مبنى فيه حارس على الباب. الحارس عنده قاعدة بسيطة: أي حد جاي من نفس المبنى يعدّي على طول. لكن أي زائر جاي من مبنى تاني، الحارس بيوقفه ويسأل الإدارة الأول: «فيه واحد جاي من مبنى app عايز يدخل ويعمل كذا، تسمحوا؟». لو الإدارة قالت «آه، اسمه في القائمة»، يعدّيه. لو مردّتش أو قالت لأ، يرجّعه من غير ما يدخل.
المتصفح هو الحارس. مبناك (موقعك) هو «نفس المبنى». والـ API اللي على نطاق تاني هو «المبنى التاني». الحارس مش بيمنعك عشان يزعجك، بيمنعك عشان لو موقع خبيث فتحته بالغلط حاول يكلّم بنك أونلاين انت مسجّل فيه، الحارس يوقفه قبل ما يسرق بياناتك.
الشرح العلمي: سياسة نفس الأصل (Same-Origin Policy)
«الأصل» (Origin) مش الدومين بس. هو تلات حاجات مع بعض: البروتوكول + الاستضافة + المنفذ. يعني https://app.example.com أصل مختلف تمامًا عن http://app.example.com (بروتوكول مختلف)، وعن https://api.example.com (استضافة مختلفة)، وعن https://app.example.com:8080 (منفذ مختلف).
المتصفح بيطبّق قاعدة اسمها Same-Origin Policy: كود جافاسكريبت في صفحة من أصل معيّن، ممنوع يقرا رد طلب رايح لأصل تاني، إلا لو الأصل التاني صرّح بوضوح إنه موافق. التصريح ده بيتم عن طريق ترويسات CORS في رد السيرفر. يعني CORS مش «حماية» بتضيفها، هو استثناء منظّم من قاعدة المنع الافتراضية.
الطلب بيتبعت مرتين: قصة الـ Preflight
لطلبات معيّنة (زي DELETE، أو POST بترويسة Content-Type: application/json)، المتصفح بيعمل حاجة بتلخبط المبتدئين: بيبعت طلب قبل الطلب الحقيقي، اسمه Preflight. الطلب ده أسلوبه OPTIONS، وبيسأل السيرفر: «هل تسمح لـ app.example.com إنه يعمل DELETE، ويبعت ترويسة Authorization؟».
ارجع لمثال الحارس: ده بالظبط لما الحارس يكلّم الإدارة قبل ما يدخّل الزائر. لو الإدارة وافقت، الزائر يعدّي. علميًا: المتصفح بيبعت OPTIONS الأول، والسيرفر لازم يرد بترويسات Access-Control-Allow-*. لو الرد عجب المتصفح، بيبعت الطلب الحقيقي. يعني طلب واحد بقى اتنين.
ده مهم عمليًا: لو صفحتك بتعمل 30 طلب API محتاج preflight، ممكن يتحوّلوا لـ 60 طلب فعلي على الشبكة. على شبكة 4G، كل preflight بيضيف حوالي 120–250 مللي ثانية زيادة (round-trip كامل زيادة). الحل إنك تخلّي السيرفر يقول للمتصفح «خزّن الموافقة دي فترة» بترويسة Access-Control-Max-Age، فالمتصفح ما يكررش الـ preflight لكل طلب.
إزاي تصلّحها صح (من السيرفر)
الحل مش في الفرونت، الحل في السيرفر. لازم السيرفر يبعت الترويسات اللي بتسمح لأصلك بالظبط. مثال على Express بمكتبة cors:
const cors = require('cors');
app.use(cors({
origin: 'https://app.example.com', // الأصل المسموح له بالظبط
methods: ['GET', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // فعّلها بس لو بتبعت كوكيز
maxAge: 86400 // خزّن رد الـ preflight 24 ساعة
}));ده بيخلّي السيرفر يرد بترويسات كده:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400لو سيرفرك Nginx مباشرة، نفس الفكرة:
location /api/ {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "86400" always;
if ($request_method = OPTIONS) { return 204; } # رد سريع على الـ preflight
}عايز تشوف بعينك اللي بيحصل قبل ما تصلّح؟ شغّل الأمر ده وشوف الترويسات الراجعة:
curl -i -X OPTIONS https://api.example.com/orders \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: DELETE"لو الرد مفيهوش سطر Access-Control-Allow-Origin، دي مشكلتك بالظبط.
الغلط الشائع: Access-Control-Allow-Origin: *
أول حل بيلاقيه المبتدئ على الإنترنت هو Access-Control-Allow-Origin: *، يعني «اسمح لأي موقع». الطريقة دي بتحل الخطأ فورًا، وعشان كده بتنتشر. بس عندها فخّين:
- بتفشل مع الكوكيز. لو محتاج تبعت credentials (كوكيز أو Authorization بجلسة)، المتصفح بيرفض
*تمامًا. لازم تحدّد الأصل بالاسم معAccess-Control-Allow-Credentials: true. النجمة والكوكيز مايجتمعوش. - بتفتح API عام لأي موقع. أي صفحة على النت تقدر تقرا رد الـ API بتاعك من متصفح المستخدم.
الـ trade-off هنا واضح: * بيوفّر عليك دقيقة إعداد، لكن بتخسر التحكم في مين يكلّم API بتاعك. لو الـ API عام تمامًا وبدون كوكيز (زي API لأسعار العملات)، النجمة مقبولة. غير كده، حدّد الأصل بالاسم.
متى لا تستخدم هذه الطريقة
لو الفرونت والـ API ممكن يكونوا على نفس الأصل، فالأنظف إنك تشيل مشكلة CORS من أساسها بدل ما تعالجها. خلّي الـ API تحت نفس الدومين عن طريق reverse proxy، مثلاً https://app.example.com/api/ بدل نطاق منفصل. كده الطلب مبقاش عبر-أصل، ومفيش preflight ولا ترويسات CORS خالص، وبتكسب أبسط وأسرع. كمان لو الاتصال بين سيرفر وسيرفر (مش من متصفح)، فـ CORS مالوش أي علاقة بالموضوع من أصله، لأنه قاعدة متصفح بس.
الخطوة التالية
افتح الـ DevTools في متصفحك، روح تاب Network، دوّر على الطلب اللي بيفشل، وبصّ في قسم Response Headers. لو مفيش Access-Control-Allow-Origin بقيمة أصلك، ضيفه على السيرفر بالأصل المحدد (مش بنجمة) وأعد التجربة. لو لسه فيه خطأ، بصّ على طلب الـ OPTIONS الأول، لأن مشكلتك غالبًا في رد الـ preflight نفسه.
المصادر
- MDN — Cross-Origin Resource Sharing (CORS): developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- MDN — Preflight request: developer.mozilla.org/en-US/docs/Glossary/Preflight_request
- MDN — Access-Control-Allow-Origin: developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
- MDN — Same-origin policy: developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
- WHATWG — Fetch Standard (CORS protocol): fetch.spec.whatwg.org/#http-cors-protocol