لو فتحت الـ console في المتصفح ولقيت السطر الأحمر ده: Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy، يبقى أنت قدام أشهر مفاجأة بتحصل لأي مطور frontend جديد. السيرفر شغّال بدليل إنه بيرد على Postman، الكود سليم، ومع ذلك المتصفح بيرفض. السبب مش bug، السبب طبقة حماية اسمها CORS وانت محتاج تفهمها قبل ما تكتب أول API call في حياتك.
CORS للمبتدئ: ليه المتصفح بيرفض طلبك رغم إن السيرفر شغّال
المشكلة باختصار
لما تكتب fetch('https://api.example.com/users') من صفحة شغّالة على localhost:3000، المتصفح بيلاحظ إن الـ Origin بتاع الصفحة مش زي الـ Origin بتاع الـ API. الأولى http://localhost:3000، التانية https://api.example.com. ساعتها بيدخل في وضع تأمين خاص، وبيرفض يديلك الرد إلا لو السيرفر صرّح صراحة إنه موافق يستقبل طلبات من الـ Origin بتاعك. الرفض ده بيتم على مستوى المتصفح، مش على مستوى السيرفر. يعني السيرفر فعلاً استقبل الطلب ورد، بس المتصفح أكل الرد ومنعه يوصلك.
مثال بسيط: بوّاب البناية
تخيّل إنك ساكن في عمارة، والبوّاب عنده تعليمات صارمة: ميسمحش لحد من بنايات تانية يدخل شقتك، إلا لو صاحب الشقة سبق وقال للبوّاب صراحةً: "الراجل اللي اسمه أحمد من العمارة المقابلة مسموحله يدخل عندي". لو أحمد جه ومحدّش قال للبوّاب عنه قبل كده، البوّاب هيوقفه عند الباب ويرجّعه، حتى لو صاحب الشقة فاتح الباب وقال "تفضل ادخل".
المتصفح هو البوّاب. الصفحة شقتك. الـ API بتاعك بناية تانية. الـ Origin هو عنوان البناية. وقاعدة "مسموحله يدخل عندي" هي اللي بتترجم لـ HTTP header اسمه Access-Control-Allow-Origin. لو السيرفر مبعتش الـ header ده، المتصفح بيرجّع الرد على الباب وميديهوش لكودك، مهما كان الرد فيه إيه.
تعريف Origin بدقة
الـ Origin مش الدومين بس. هو 3 حاجات لازم يتطابقوا كلهم:
- الـ scheme (يعني
httpأوhttps) - الـ host (يعني
example.comأوapi.example.com) - الـ port (يعني
:3000أو:8080)
يعني http://example.com و https://example.com أصلهم اتنين Origin مختلفين رغم إن الدومين واحد، لأن الـ scheme اختلف. وكذلك http://localhost:3000 و http://localhost:4000 Origins مختلفين رغم إن الـ host واحد، لأن الـ port اختلف. التعريف ده موثّق في RFC 6454 ومُطبَّق نصاً في كل المتصفحات الحديثة.
ليه المتصفح بيتصرف كده أصلاً؟ Same-Origin Policy
القاعدة دي اسمها Same-Origin Policy وهي موجودة من 1995 في Netscape. الهدف منها يمنع موقع خبيث من قراءة بياناتك من موقع تاني انت مسجّل دخول فيه. مثال خطير: لو فتحت تبويبة فيها بنكك، وفي نفس الوقت فتحت تبويبة تانية فيها موقع مشبوه. لو مفيش Same-Origin Policy، الموقع المشبوه يقدر يبعت fetch لـ API البنك بتاعك ويستخدم الـ cookies بتاعتك، ويسحب رصيدك. الـ Same-Origin Policy هي اللي بتقفل ده.
CORS (اختصار Cross-Origin Resource Sharing) جاي يقول للمتصفح: "في الحالات اللي السيرفر بنفسه موافق فيها، اسمح بالاتصال". يعني CORS مش حماية، CORS هو الباب المسموح اللي بيخفّف من صرامة Same-Origin Policy في حالات محددة. الفهم ده مهم جداً، لأن أكتر مطورين بيفتكروا إن CORS بيمنعهم، والحقيقة Same-Origin Policy هي اللي بتمنع، و CORS هو اللي بيسمح.
3 أنواع طلبات CORS لازم تعرفهم
1) Simple Request
طلب GET أو HEAD أو POST بـ Content-Type عادي زي text/plain أو application/x-www-form-urlencoded، وبدون headers مخصصة. هنا المتصفح بيبعت الطلب على طول، والسيرفر لازم يرد بـ Access-Control-Allow-Origin: http://localhost:3000 علشان المتصفح يسلّم الرد لكودك.
2) Preflight Request
أي طلب فيه Content-Type: application/json أو فيه method زي PUT أو DELETE، أو فيه custom header زي Authorization، المتصفح بيبعت طلب OPTIONS قبله. الـ OPTIONS ده اسمه preflight ومعناه "بسأل قبل ما أبعت". لو السيرفر رد على الـ OPTIONS بـ Access-Control-Allow-Methods و Access-Control-Allow-Headers فيهم اللي المتصفح بيسأل عنه، ساعتها يبعت الطلب الحقيقي. لو لأ، الطلب الحقيقي مبيتبعتش أصلاً. ده اللي بيخلّي بعض الطلبات تبان وكأنها مرّتش بالسيرفر — هي فعلاً مرّتش، لأن الـ preflight فشل.
3) Credentialed Request
لو حطّيت credentials: 'include' في الـ fetch علشان يبعت cookies، السيرفر لازم يرجّع Access-Control-Allow-Credentials: true، و ممنوع يحط Access-Control-Allow-Origin: *، لازم يحط الـ Origin بالاسم. لو الشرطين مش متوفرين، المتصفح بيرفض. المنع ده تأمين متعمد.
الحل من جنب السيرفر: مثال Express 5 شغّال
// server.js — Node.js 22 + Express 5.0
import express from 'express';
import cors from 'cors';
const app = express();
// إعداد CORS صريح بدل ما تستخدم * الخطر
app.use(cors({
origin: ['http://localhost:3000', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // كاش الـ preflight لـ 24 ساعة
}));
app.get('/api/users', (req, res) => {
res.json({ users: ['أحمد', 'سارة', 'محمد'] });
});
app.listen(8080, () => console.log('Server on :8080'));
الـ maxAge: 86400 ده مهم. معناه إن المتصفح هيكاش رد الـ preflight لمدة 24 ساعة، فمش هيبعت OPTIONS كل مرة. لو معملتهوش، كل طلب لازم يسبقه preflight، وده بيضيف 80–200 مللي ثانية على كل request.
الحل في FastAPI (Python)
# app.py — Python 3.12 + FastAPI 0.110
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400,
)
@app.get("/api/users")
def get_users():
return {"users": ["أحمد", "سارة", "محمد"]}
الـ trade-offs اللي محدش بيقولك عليها
- استخدام
*سهل بس خطر.Access-Control-Allow-Origin: *بيفتح الـ API لأي موقع في الدنيا. مناسب لـ public API بدون مصادقة، كارثة لأي endpoint فيه auth. الـ trade-off: تستخدمه ⇐ تكسب 5 دقايق إعداد، تخسر طبقة حماية بتمنع 90% من هجمات CSRF. - الـ preflight بيكلّفك latency. كل request مع
Content-Type: application/jsonبيتسبق بـ OPTIONS. على workload فيه 1,000 طلب/دقيقة و RTT 80 مللي ثانية، ده 80 ثانية ضائعة في الدقيقة. الحل:maxAgeكبير، أو دمج طلبات. - الـ proxy في dev بيخفي المشكلة. ضبط
"proxy": "http://localhost:8080"فيpackage.jsonبيخلّي الـ frontend يبعت لنفس الـ Origin، فالمتصفح ميشتكيش. النتيجة: الكود شغّال محلياً ومش شغّال في الإنتاج. الـ trade-off: راحة dev مقابل مفاجأة prod. - CORS مش بديل عن authentication. CORS بيقول مين يقدر يطلب، مش مين يقدر يدخل. لو سيبت
origin: '*'وفاكر إن كده الـ API محمي، فأي bot يقدر يضربك من server-side (cURL مفيهوش CORS أصلاً، الـ Same-Origin Policy ده تخصص متصفح بس).
متى لا يكون CORS هو السبب
3 حالات الناس بتفتكرها CORS وهي مش CORS:
- السيرفر بيرجّع 500 internal error. ده مش CORS، ده bug في الـ backend. CORS بيظهر بس لما الرد صح (200/201) والمتصفح بيمنعه.
- الـ Network tab بيقول
net::ERR_CONNECTION_REFUSED. ده معناه السيرفر مش شغّال أصلاً أو الـ port غلط، مالوش علاقة بـ CORS. - أنت بتطلب من
file://(يعني فتحت index.html بدوبل كليك). الـfile://ملوش Origin معتبر، فالمتصفحات بتعمل لها معاملة خاصة. الحل: شغّل live server علىhttp://localhost.
الخطوة التالية
افتح الـ Network tab في المتصفح، ابعت طلب من الـ frontend، وادوّر على request method اسمه OPTIONS. لو لقيته فاشل بـ 403 أو 404، يبقى السيرفر مش بيتعامل مع preflight. ضيف middleware الـ CORS اللي فوق وأعد المحاولة. لو OPTIONS رجع 200 بس الطلب الأصلي اتمنع، شوف رد الـ OPTIONS من جوّا واتأكد إن Access-Control-Allow-Origin فيه الـ Origin بتاعك بالظبط.