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

٢١ يوليو ٢٠٢٦ · آخر تحديث: ١٩ أكتوبر ٢٠٢٦

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

شارك المقال:

كل من ربط واجهة أمامية بخادم 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-CredentialsAccess-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: تعمل على جهازك وحدك، وتخفي المشكلة حتى يوم النشر.

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

  1. اقرأ الجملة بعد «blocked by CORS policy:» وطابقها مع الجدول.
  2. تبويب Network: هل يوجد طلب OPTIONS؟ ما حالته وترويساته؟
  3. أضف نطاقك لقائمة بيضاء على الخادم، مع Vary: Origin.
  4. كوكيز؟ نطاق صريح + Allow-Credentials: true + credentials: "include".
  5. لا تملك الخادم؟ proxy من نفس النطاق.

CORS كمفهوم بالتفصيل في درس CORS ضمن مسار REST API، وجانبه الأمني في إساءة إعداد CORS. وإن كان الرد يصل لكن ليس JSON فراجع Unexpected token in JSON.

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

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

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

لأن CORS قيد يفرضه المتصفح فقط. Postman وcurl ليسا صفحة ويب قد يستغلها موقع خبيث، فلا يطبقانه أصلًا. الخادم يرد بالبيانات نفسها في الحالتين، والفرق أن المتصفح يفحص ترويسات الرد قبل أن يسلّمه لكود صفحتك.

هل الطلب يصل للخادم رغم خطأ CORS؟

في الطلبات البسيطة (GET وPOST عاديين) نعم: جرّبنا POST محجوبًا، فوصل للخادم ونُفّذ، والمتصفح منع قراءة الرد فقط. لذلك CORS ليس حماية من CSRF. أما الطلبات التي تحتاج preflight (PUT، أو ترويسة Authorization) فلا يُرسل الطلب الحقيقي إن فشل OPTIONS: الخادم رأى OPTIONS وحده.

هل Access-Control-Allow-Origin: * آمن؟

آمن لموارد عامة لا تحتاج مصادقة. مع الكوكيز (credentials: 'include') يرفضه المتصفح أصلًا: جرّبناه فظهرت رسالة must not be the wildcard '*' when the request's credentials mode is 'include'. حدّد النطاق المسموح من قائمة بيضاء، ولا تعكس أي Origin يصلك بلا فحص.

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

غالبًا طلب OPTIONS (preflight). اقرأ الرسالة: Response to preflight request doesn't pass access control check تعني أن رد OPTIONS نفسه ناقص، أو 404، أو تحويل (Redirect is not allowed for a preflight request). وإن ظهرت Request header field authorization is not allowed فأضف Authorization إلى Access-Control-Allow-Headers.

اقرأ أيضًا

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