كل من ربط واجهة أمامية بخادم API على نطاق مختلف صادف هذه الرسالة في Console
المتصفح. ويكفي أن يختلف المنفذ فقط ليختلف النطاق: localhost:3000 و
localhost:5000 نطاقان مختلفان.
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.
الخبر الجيد: الرسالة دقيقة جدًا بمجرد أن تعرف من يصدرها، وأن الجملة بعد النقطتين هي التشخيص كاملًا.
من يمنع الطلب فعليًا؟
المتصفح، لا خادمك. الافتراض أن صفحة من app.example.com لا تستطيع قراءة رد
من api.example.com. هذه سياسة المصدر الواحد (Same-Origin Policy)، وهدفها منع
موقع خبيث من قراءة بياناتك من موقع آخر أنت مسجّل دخولك فيه.
CORS (Cross-Origin Resource Sharing) هي الطريقة التي يفتح بها الخادم استثناءً محسوبًا: ترويسات في الرد تقول للمتصفح «هذا النطاق مسموح له بالقراءة».
جرّبنا ذلك فعليًا في Chromium: صفحة على 127.0.0.1:4700 ترسل POST إلى خادم على
127.0.0.1:4701 لا يرسل ترويسات CORS. سجل الخادم أظهر أن الطلب وصل ونُفّذ،
والمتصفح منع كود الصفحة من قراءة الرد فقط، فرمى fetch الخطأ
TypeError: Failed to fetch. نتيجتان مهمتان:
- الحل من الخادم لا من الواجهة. لا يوجد إعداد في
fetchأوaxiosيجعل المتصفح يتجاهل القاعدة. - CORS ليس حماية لخادمك. الطلب البسيط نُفّذ رغم «الحجب». حماية العمليات الحساسة تكون بالمصادقة ورموز CSRF، لا بـ CORS.
اقرأ آخر الرسالة: لكل جملة سبب
كل رسالة هنا ظهرت لنا في Chromium بخادم تجريبي ضبطناه لإنتاجها:
| ما بعد «blocked by CORS policy:» | السبب | الحل |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present on the requested resource. | الخادم لم يرسل الترويسة إطلاقًا | أضفها بنطاق واجهتك |
The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard '*' when the request's credentials mode is 'include'. | * مع كوكيز | نطاق صريح بدل * |
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' ... | كوكيز بلا Allow-Credentials | Access-Control-Allow-Credentials: true |
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response. | رد OPTIONS لا يذكر الطريقة | أضفها لـ Allow-Methods |
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. | ترويسة مخصّصة غير مصرّح بها | أضفها لـ Allow-Headers |
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' ... | OPTIONS نفسه بلا ترويسات (أو 404) | اجعل الخادم يرد على OPTIONS |
Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request. | OPTIONS حُوّل (301/302) | الرابط الصحيح مباشرة (https، والشرطة الأخيرة) |
وإن ظهر net::ERR_CONNECTION_REFUSED بدل كل هذا، فليس CORS أصلًا: الخادم لا يعمل.
(دليله هنا.)
الحل: من إعدادات الخادم
يدويًا في Express (لتفهم ما يحدث)
const ALLOWED = new Set(["https://app.example.com", "http://localhost:3000"]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin"); // الرد يختلف حسب Origin
}
if (req.method === "OPTIONS") { // preflight
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
return res.sendStatus(204);
}
next();
});
- قائمة بيضاء، لا انعكاس أعمى. نسخ أي
Originيصلك إلى الترويسة يعادل*، لكن مع الكوكيز، وهذا أسوأ. Vary: Originتخبر أي cache (CDN أو المتصفح) أن الرد يختلف حسب النطاق، وإلا قد يُخزَّن رد نطاق ويُقدَّم لنطاق آخر.OPTIONSيُرد عليه قبل المصادقة. لو مرّ طلب OPTIONS على وسيط يشترط تسجيل الدخول لأعاد 401، والمتصفح لا يرسل الكوكيز ولاAuthorizationمع preflight.
بحزمة cors (الأنسب عمليًا)
import cors from "cors";
app.use(cors({
origin: ["https://app.example.com", "http://localhost:3000"],
credentials: true, // إن كانت الواجهة ترسل كوكيز
}));
جرّبناها: لنطاق في القائمة أعادت Access-Control-Allow-Origin بالنطاق نفسه و
Vary: Origin، وردّت على OPTIONS بـ 204 مع Allow-Methods وAllow-Headers. ولنطاق
خارج القائمة لم تضع الترويسة، فيحجب المتصفح القراءة. لاحظ أن الخادم ما زال يرد
بالبيانات بالحالة 200: CORS قرار يتخذه المتصفح.
FastAPI وDjango
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Django: pip install django-cors-headers
INSTALLED_APPS = [..., "corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...] # في أعلى القائمة
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]
جرّبنا إعداد FastAPI أعلاه: أعاد Allow-Origin لنطاق الواجهة، وردّ على OPTIONS
بالحالة 200 مع authorization في Allow-Headers. وفي Django ضع CorsMiddleware
قبل أي وسيط يكتب ردًا، وإلا خرج الرد بلا ترويسات.
متى يرسل المتصفح OPTIONS (preflight)؟
الطلب «البسيط» (GET، أو POST بنموذج عادي) يُرسل مباشرة. أما هذه فتحتاج فحصًا مسبقًا
بطلب OPTIONS:
- الطرق
PUTوDELETEوPATCH. - أي ترويسة مخصّصة، وأشهرها
Authorization. Content-Type: application/json، وهذا يفاجئ الجميع: كلPOSTبـ JSON يحتاج preflight.
وإن فشل الـ preflight لا يُرسل الطلب الحقيقي أصلًا. جرّبنا PUT مع
Authorization إلى خادم لا يصرّح بها، فلم يرَ الخادم إلا طلب OPTIONS. افتح تبويب
Network (F12) وفلتر على OPTIONS: إن كان رده 404 أو 401 أو 301، فهذا السبب.
الكوكيز وتسجيل الدخول: ثلاثة شروط معًا
إن كانت جلستك تعتمد على كوكيز والواجهة على نطاق آخر:
// الواجهة
fetch("https://api.example.com/me", { credentials: "include" });
# رد الخادم
Access-Control-Allow-Origin: https://app.example.com ← نطاق صريح، لا *
Access-Control-Allow-Credentials: true
Vary: Origin
نقص أي واحد منها يعطي رسالة من الجدول أعلاه. وانتبه لإعداد الكوكي نفسه: إن كانت
الواجهة والـ API على موقعين مختلفين تمامًا (myapp.com وmyapi.net، لا نطاقين
فرعيين لموقع واحد)، فكوكي الجلسة يحتاج SameSite=None; Secure، وإلا لن يرسله
المتصفح مع الطلب.
لا تملك الخادم؟ proxy من نفس النطاق
CORS قيد على المتصفح، فالطلب من خادم إلى خادم لا يخضع له. في التطوير، اجعل
خادم التطوير يمرّر طلبات /api للـ API، فتصبح الطلبات من نفس النطاق:
// vite.config.js
export default {
server: {
proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } },
},
};
// next.config.js
module.exports = {
async rewrites() {
return [{ source: "/api/:path*", destination: "http://localhost:5000/:path*" }];
},
};
ثم اطلب fetch("/api/data") بمسار نسبي، لا بالرابط الكامل. وفي الإنتاج لخدمة طرف
ثالث لا تتحكم فيها: معالج مسار في خادمك يطلب البيانات ويمرّرها (وهناك تحفظ مفتاح
الـ API بعيدًا عن المتصفح أيضًا).
ولا تستخدم إضافات المتصفح التي تعطّل CORS: تعمل على جهازك وحدك، وتخفي المشكلة حتى يوم النشر.
خلاصة التشخيص السريع
- اقرأ الجملة بعد «blocked by CORS policy:» وطابقها مع الجدول.
- تبويب Network: هل يوجد طلب
OPTIONS؟ ما حالته وترويساته؟ - أضف نطاقك لقائمة بيضاء على الخادم، مع
Vary: Origin. - كوكيز؟ نطاق صريح +
Allow-Credentials: true+credentials: "include". - لا تملك الخادم؟ proxy من نفس النطاق.
CORS كمفهوم بالتفصيل في درس CORS ضمن مسار REST API، وجانبه الأمني في إساءة إعداد CORS. وإن كان الرد يصل لكن ليس JSON فراجع Unexpected token in JSON.