DEV Community

Cover image for كيفية إصلاح أخطاء CORS: تصحيح Access-Control-Allow-Origin
Yusuf Khalidd
Yusuf Khalidd

Posted on Originally published at apidog.com

كيفية إصلاح أخطاء CORS: تصحيح Access-Control-Allow-Origin

فهم أخطاء CORS وإصلاحها: دليل عملي لـ Express وSpring Boot وNginx

تنشر واجهة أمامية جديدة، تفتح وحدة التحكم (console)، فتجد خطأ CORS أحمر يخبرك بأن الطلب «تم حظره بواسطة سياسة CORS». تعمل واجهة برمجة التطبيقات (API) في Apidog أو curl، لكن المتصفح يرفض تسليم الاستجابة إلى JavaScript. السبب غالبًا بسيط: الخادم لم يرسل رؤوس Access-Control-Allow-Origin الصحيحة.

جرّب Apidog اليوم

الحقيقة المهمة: يفرض المتصفح CORS، لكن الخادم هو سبب الخطأ. لذلك يكون الإصلاح عادةً في تكوين الخادم، لا في كود الواجهة الأمامية.

في هذا الدليل ستتعلم:

  • كيف يعمل CORS وطلب الفحص المسبق (preflight).
  • كيفية إصلاح أخطاء CORS الستة الأكثر شيوعًا.
  • تكوين CORS في Express وSpring Boot وNginx.
  • تصحيح الأخطاء خارج المتصفح باستخدام Apidog.

للتفاصيل الرسمية، راجع وثائق MDN حول CORS ومواصفات Fetch.

ما هو CORS؟

CORS هو اختصار لـ Cross-Origin Resource Sharing، أي مشاركة الموارد عبر المصادر.

تفرض المتصفحات افتراضيًا سياسة نفس المصدر (same-origin policy). لذلك لا يمكن لـ JavaScript يعمل على:

https://app.example.com
Enter fullscreen mode Exit fullscreen mode

قراءة استجابة من:

https://api.example.com
Enter fullscreen mode Exit fullscreen mode

لأن المخطط (scheme) أو المضيف (host) أو المنفذ (port) مختلف.

تذكّر النقاط التالية:

  • المتصفح هو من يفرض CORS: طلبات الخادم إلى الخادم وcurl وعملاء API لسطح المكتب لا يطبقون هذه الفحوصات.
  • الخادم هو من يكوّن CORS: يقرر المتصفح السماح بناءً على رؤوس الاستجابة.
  • الطلب قد يصل إلى الخادم رغم الخطأ: في الطلبات البسيطة قد يعالج الخادم الطلب ويرسل الاستجابة، ثم يمنع المتصفح JavaScript من قراءتها.
  • CORS ليس جدارًا أمنيًا للـ API: هو يحمي المستخدمين من صفحات خبيثة تحاول قراءة بياناتهم عبر المصادر باستخدام ملفات تعريف الارتباط الخاصة بهم.

عند ظهور خطأ CORS، افحص رسالة الخطأ وأصلح الرأس المفقود أو الخاطئ على الخادم بدل البحث عن حل التفافي في الواجهة الأمامية.

تشريح طلب الفحص المسبق

قبل بعض الطلبات عبر المصادر، يرسل المتصفح طلب OPTIONS استكشافيًا يسمى الفحص المسبق (preflight).

يُرسل الفحص المسبق عادةً عندما:

  • تستخدم طريقة غير GET أو HEAD أو POST.
  • ترسل رؤوسًا مخصصة مثل Authorization.
  • تستخدم Content-Type: application/json.

مثال:

OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
Enter fullscreen mode Exit fullscreen mode

إجابة الخادم المتوقعة:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
Enter fullscreen mode Exit fullscreen mode

إذا كان أي جزء مطلوب مفقودًا، يلغي المتصفح الطلب الفعلي قبل تشغيله. قد ترى طلب OPTIONS في السجلات، لكن نقطة نهاية API لن تُستدعى.

يحدد Access-Control-Max-Age مدة تخزين نتيجة الفحص المسبق مؤقتًا بالثواني. في المثال، سيخزن المتصفح القرار لمدة 86400 ثانية.

السؤال الأساسي عند تصحيح CORS هو:

هل فشل الفحص المسبق، أم فشل الطلب الفعلي؟

أخطاء CORS الستة الأكثر شيوعًا

1. عدم وجود رأس Access-Control-Allow-Origin

أرسل الخادم استجابة بلا رؤوس CORS، فلم يجد المتصفح ما يقيّمه.

أرسل مصدرًا محددًا:

Access-Control-Allow-Origin: https://app.example.com
Enter fullscreen mode Exit fullscreen mode

أو استخدم * فقط مع واجهات API عامة لا تستخدم بيانات اعتماد.

احرص على إضافة الرؤوس إلى كل الاستجابات، بما فيها 401 و403 و500. إذا أضافت Middleware رؤوس CORS إلى استجابات النجاح فقط، فقد يعرض المتصفح خطأ CORS بدل خطأ الخادم الحقيقي. راجع أيضًا شرح حالة 403 Forbidden.

2. استخدام * مع بيانات الاعتماد

يحدث ذلك عندما ترسل الواجهة الأمامية:

fetch(url, { credentials: 'include' });
Enter fullscreen mode Exit fullscreen mode

بينما يرد الخادم بـ:

Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

هذا الاقتران غير مسموح به. استخدم المصدر الفعلي وأضف:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Enter fullscreen mode Exit fullscreen mode

تحقق من قيمة Origin الواردة مقابل قائمة مصادر مسموح بها، ولا تعكس مصادر عشوائية مع تفعيل بيانات الاعتماد.

3. فشل الفحص المسبق في اختبار التحكم بالوصول

قد لا يتعامل الخادم مع OPTIONS، فيعيد 404 أو 405. وقد ترفضه طبقة المصادقة بـ401 لأن الفحص المسبق لا يحمل رمز المصادقة؛ فالمتصفح لا يرسل بيانات الاعتماد مع طلبات الفحص المسبق.

عالج OPTIONS قبل تشغيل المصادقة:

app.options('/v1/orders', (req, res) => {
  res.set({
    'Access-Control-Allow-Origin': 'https://app.example.com',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Authorization, Content-Type'
  });
  res.sendStatus(204);
});
Enter fullscreen mode Exit fullscreen mode

في معظم الأطر، يكفي تركيب Middleware الخاص بـ CORS قبل Middleware المصادقة.

4. عدم تطابق قيمة الرأس مع المصدر

قد يرسل الخادم رأس Access-Control-Allow-Origin، لكنه يحدد مصدرًا مختلفًا. من الأسباب الشائعة:

  • اختبار http://localhost:5173 بينما الخادم يسمح فقط بمصدر الإنتاج.
  • اختلاف http وhttps.
  • اختلاف المنفذ.
  • وجود شرطة مائلة زائدة؛ فـhttps://app.example.com/ ليست قيمة مصدر صالحة.

استخدم قائمة مصادر صريحة وأعد المصدر المطابق:

const allowed = [
  'https://app.example.com',
  'http://localhost:5173'
];

if (allowed.includes(req.headers.origin)) {
  res.set('Access-Control-Allow-Origin', req.headers.origin);
  res.set('Vary', 'Origin');
}
Enter fullscreen mode Exit fullscreen mode

يساعد Vary: Origin ذاكرة التخزين المؤقت وشبكات CDN على عدم تقديم استجابة مصدر إلى مصدر آخر.

5. عدم السماح بطريقة أو رأس مطلوب

قد تظهر رسائل مثل:

Request header field authorization is not allowed by Access-Control-Allow-Headers
Enter fullscreen mode Exit fullscreen mode

أو:

Method PUT is not allowed by Access-Control-Allow-Methods
Enter fullscreen mode Exit fullscreen mode

وسّع استجابة الفحص المسبق لتشمل كل ما ترسله الواجهة الأمامية:

Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Enter fullscreen mode Exit fullscreen mode

أسماء الرؤوس غير حساسة لحالة الأحرف، أما أسماء الطرق فهي حساسة لحالة الأحرف ويُفضّل كتابتها بأحرف كبيرة.

6. إعادة توجيه طلب الفحص المسبق

إذا أعاد عنوان الفحص المسبق رمز 301 أو 302، فقد يرفض المتصفح متابعة إعادة التوجيه.

الأسباب المعتادة:

  • استخدام عنوان http يعيد التوجيه إلى https.
  • إضافة أو حذف شرطة مائلة زائدة في المسار.
  • إعادة توجيه بوابة /v1/orders إلى /v1/orders/.

الإصلاح:

  1. استخدم عنوان https النهائي مباشرة.
  2. طابق اتفاقية الشرطة المائلة الخاصة بالموجه.
  3. اختبر طلب OPTIONS وتأكد من حصوله على 2xx بدل 3xx.

أمثلة تكوين الخادم

Express

استخدم حزمة CORS الرسمية بدل كتابة الرؤوس يدويًا:

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
  origin: ['https://app.example.com', 'http://localhost:5173'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400
}));
Enter fullscreen mode Exit fullscreen mode

ركّبها قبل Middleware المصادقة حتى لا ترفض الفحوصات المسبقة بسبب غياب الرموز.

في تطبيقات Flask، يوفر امتداد Flask-CORS نمطًا مشابهًا.

Spring Boot

يمكنك استخدام WebMvcConfigurer:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/v1/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type")
            .allowCredentials(true)
            .maxAge(86400);
    }
}
Enter fullscreen mode Exit fullscreen mode

إذا كنت تستخدم Spring Security، أضف:

.cors(Customizer.withDefaults())
Enter fullscreen mode Exit fullscreen mode

إلى سلسلة فلاتر الأمان. وإلا قد تمنع طبقة الأمان الفحص المسبق قبل وصوله إلى إعدادات MVC. راجع وثائق Spring CORS للتفاصيل.

Nginx

إذا كان Nginx أمام التطبيق، يمكنك معالجة الفحص المسبق عند الحافة:

location /v1/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Max-Age 86400 always;
        return 204;
    }

    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Vary "Origin" always;
    proxy_pass http://backend;
}
Enter fullscreen mode Exit fullscreen mode

كلمة always مهمة؛ فمن دونها لا يضيف Nginx الرؤوس إلى استجابات 4xx و5xx.

اختر طبقة واحدة لإدارة CORS. إذا أضاف كل من Nginx والتطبيق الرؤوس، فقد تحصل على قيم مكررة مثل:

Access-Control-Allow-Origin: *, *
Enter fullscreen mode Exit fullscreen mode

ويرفض المتصفح الاستجابة.

تصحيح أخطاء CORS باستخدام Apidog

يخبرك خطأ وحدة التحكم بأن المتصفح حظر الاستجابة، لكنه لا يعرض دائمًا ما أرسله الخادم. يزيل Apidog المتصفح من المعادلة، لأن طلباته لا تخضع لفحوصات CORS الخاصة بالمتصفح.

إذا نجح الطلب في Apidog، فغالبًا منطق API سليم والمشكلة في رؤوس CORS. وإذا فشل هناك أيضًا، فالمشكلة خطأ عادي في API وليست CORS فقط. راجع دليل تقنيات اختبار API.

خطوات التصحيح

  1. أعد إنشاء الطلب الحقيقي: انسخ الطلب الفاشل من Network في المتصفح إلى Apidog، مع الطريقة والرؤوس والجسم. افحص الحالة والجسم؛ فرمز 500 يعني أن لديك خطأ خادم حقيقي.
  2. اختبر الفحص المسبق يدويًا: أنشئ طلب OPTIONS وأضف:
   Origin: https://app.example.com
   Access-Control-Request-Method: POST
   Access-Control-Request-Headers: authorization, content-type
Enter fullscreen mode Exit fullscreen mode
  1. افحص الاستجابة: ابحث عن:
   Access-Control-Allow-Origin
   Access-Control-Allow-Methods
   Access-Control-Allow-Headers
Enter fullscreen mode Exit fullscreen mode
  1. تحقق من الإصلاح: بعد تعديل إعدادات الخادم، أعد إرسال طلب OPTIONS المحفوظ وتأكد من تحديث الرؤوس.

هذه الطريقة تحسم بسرعة مشكلة «يعمل في عميل API ويفشل في المتصفح». يعمل العميل لأنه يتجاوز CORS، بينما يحتاج المتصفح إلى موافقة الخادم عبر الرؤوس الصحيحة. يمكنك تنزيل Apidog والاحتفاظ بطلب OPTIONS بجوار اختبارات API العادية.

قائمة تحقق CORS في 30 ثانية

قبل فتح بلاغ، تحقق من التالي:

  • هل تحتوي الاستجابة الفاشلة على Access-Control-Allow-Origin؟
  • هل تتطابق القيمة تمامًا مع مصدر الصفحة من حيث المخطط والمضيف والمنفذ؟
  • هل توجد شرطة مائلة زائدة؟
  • عند استخدام ملفات تعريف الارتباط أو المصادقة، هل يوجد مصدر محدد وAccess-Control-Allow-Credentials: true بدل *؟
  • هل يعيد OPTIONS رمز 2xx؟
  • هل تغطي Access-Control-Allow-Methods وAccess-Control-Allow-Headers طلبك؟
  • هل توجد إعادة توجيه على عنوان URL الخاص بالفحص المسبق؟
  • هل تحتوي استجابات 401 و403 و500 على رؤوس CORS نفسها الموجودة في استجابات النجاح؟

في معظم الحالات، سيكشف طلب OPTIONS يدوي السبب مباشرة: رأس مفقود، أو مصدر غير صحيح، أو إعادة توجيه، أو طريقة غير مسموح بها.

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

لماذا يظهر خطأ CORS في المتصفح فقط؟

لأن المتصفحات هي التي تفرض CORS وسياسة نفس المصدر. تتحقق من Access-Control-Allow-Origin في الاستجابات عبر المصادر، بينما لا يطبق curl وخدمات الواجهة الخلفية وعملاء سطح المكتب القاعدة نفسها.

إذا نجح الطلب في كل مكان باستثناء المتصفح، فغالبًا تفتقد استجابة الخادم رؤوس CORS أو تحتوي على إعدادات غير صحيحة.

هل ينطبق CORS على Postman أو Apidog؟

لا. Postman وApidog تطبيقان لسطح المكتب، وليسا صفحات ويب داخل بيئة المتصفح المعزولة. لذلك تتجاوز طلباتهما CORS وتعرضان رؤوس الاستجابة الأصلية للخادم.

نجاح الطلب في عميل سطح المكتب لا يثبت أن المتصفح سيتمكن من قراءته، لكنه يساعدك على عزل طبقة المشكلة. راجع أيضًا اختبار Postman CORS.

هل خطأ CORS ميزة أمنية أم خطأ برمجي؟

هو سلوك أمني مقصود. يمنع المتصفح النصوص البرمجية من كشف بيانات الاستجابة عبر المصادر ما لم يوافق الخادم.

تعطيل CORS عبر أعلام المتصفح أو الإضافات يخفي المشكلة على جهازك فقط، ولا يصلحها للمستخدمين الآخرين. أصلح رؤوس الخادم بدلًا من ذلك.

هل يمكنني استخدام Access-Control-Allow-Origin: * في كل مكان؟

استخدمه فقط مع واجهات API عامة للقراءة لا تعتمد على ملفات تعريف الارتباط أو المصادقة.

عند استخدام بيانات الاعتماد:

  • استخدم قائمة مصادر مسموح بها.
  • أعد المصدر المطابق بدل *.
  • أرسل Access-Control-Allow-Credentials: true.
  • أضف Vary: Origin للحفاظ على فصل الاستجابات في ذاكرة التخزين المؤقت المشتركة.

Top comments (0)