Fetch API في جافا سكريبت: عميل HTTP مبني على الـ Promises
الدالة fetch متوفرة بشكل جاهز في المتصفحات وفي الإصدارات الحديثة من Node. تمرّر لها رابط URL، فتُرجع لك Promise يُحَلّ إلى كائن Response. هذه هي فكرة الـ API كلها باختصار:
استدعاءان لـ .then لأنّ العملية تمرّ بخطوتين غير متزامنتين: أولاً تصل ترويسات الاستجابة (وهذا ما يُرجعه الوعد الأول)، ثم تتم قراءة جسم الاستجابة وتحليله (ولاحظ أنّ response.json() هو نفسه وعد). فجسم الاستجابة لا يُحمَّل إلا حين تطلبه صراحةً.
ونفس المنطق مع async/await يبدو وكأنّه كود عادي يُقرأ من الأعلى إلى الأسفل:
مرتان من await، ونقطتا تعليق. نفس الشغل، لكن ترتيب القراءة أوضح.
كائن الاستجابة Response
ما يرجع لك ليس هو محتوى الاستجابة مباشرة، بل كائن Response يحمل بيانات وصفية ومجموعة من الدوال التي تتيح لك قراءة المحتوى بصيغ مختلفة:
يمكنك قراءة محتوى الاستجابة بأحد الأشكال التالية: .json() أو .text() أو .blob() أو .arrayBuffer() أو .formData(). كل واحدة منها تُرجع Promise. ولكن انتبه: يمكنك قراءة الـ body مرة واحدة فقط — إذا استدعيت .json() مرتين على نفس الاستجابة، فإن الاستدعاء الثاني سيرمي خطأً.
المفاجأة الكبرى: أخطاء HTTP لا تُعتبر فشلاً في fetch
هذه النقطة تُربك كل من يبدأ العمل مع fetch. استجابة 404 أو 500 ليست رفضاً (rejection) للـ Promise. الـ Promise يُحَلّ بشكل طبيعي، لكن القيمة response.ok === false. الـ Fetch API يرفض الطلب فقط عندما يفشل الطلب نفسه في الوصول — مثل فشل في DNS، أو انقطاع الشبكة، أو حجب من CORS.
وهذا يعني أن استخدام fetch بشكل ساذج سيمرّر لك صفحة خطأ وكأنها نتيجة ناجحة، ثم ينهار عند استدعاء .json():
الحل هو أن تفحص response.ok بنفسك وترمي استثناءً إذا رجّع الخادم حالة خطأ:
اعتد على كتابة كتلة if (!response.ok) هذه، فهي يجب أن تكون جزءًا من أي دالة تغليف لـ fetch تكتبها.
إرسال طلب POST باستخدام fetch
الطلب الافتراضي هو GET. أما إذا أردت أي نوع آخر، فمرّر وسيطًا ثانيًا عبارة عن كائن خيارات:
ثلاث نقاط تستحق الانتباه:
methodقيمته الافتراضية هي"GET". لذا عليك تحديده صراحةً عند استخدام POST أو PUT أو DELETE أو PATCH.bodyيقبل نصًا (أوFormDataأوBlobوغيرها) — دالة fetch لن تحوّل الكائنات إلى JSON نيابةً عنك. استدعاءJSON.stringify(...)مسؤوليتك أنت.- ترويسة
Content-Typeتُخبر الخادم بكيفية قراءة محتوى الطلب. إن أهملتها، ستتعامل أغلب الخوادم مع المحتوى على أنه نص عادي.
الترويسات وسلاسل الاستعلام وبقية الخيارات
الترويسات ما هي إلا كائن عادي (أو نسخة من Headers). أما سلاسل الاستعلام فتبنيها بنفسك، وعادةً عبر URLSearchParams:
URLSearchParams يتكفّل بعملية الترميز نيابةً عنك — المسافات، علامات &، ورموز اليونيكود — فلا ينتهي بك المطاف بروابط مكسورة حين يحتوي المُدخَل على محارف تحتاج إلى هروب.
من الخيارات الأخرى التي ستصادفها في الشيفرات الواقعية: credentials: "include" لإرسال ملفات تعريف الارتباط عبر النطاقات، وcache: "no-store" لتجاوز ذاكرة تخزين HTTP المؤقتة، وmode: "cors" (وهو الوضع الافتراضي عادةً) للتحكّم في سلوك CORS.
إلغاء الطلب باستخدام AbortController
أحيانًا تحتاج إلى التراجع عن الطلب — ربما كتب المستخدم استعلام بحث جديدًا، أو استغرق الطلب وقتًا أطول من اللازم. هنا يأتي دور AbortController:
استدعاء controller.abort() يرفض وعد fetch برمي DOMException يحمل الاسم "AbortError". أما كتلة finally فدورها تنظيف المؤقّت، حتى لا يترك الطلب الناجح مؤقّتًا معلّقًا في الخلفية.
هذا النمط — fetch مع مهلة زمنية مع تنظيف — يستحق أن نغلّفه في دالة مساعدة ونعيد استخدامه في كل مكان.
دالة مساعدة قابلة لإعادة الاستخدام
إذا جمعنا كل ما سبق، نحصل على دالة مساعدة صغيرة تتكفّل بالكود المتكرّر مرّة واحدة وإلى الأبد:
مكان واحد لتعديل الترويسات، مكان واحد لمعالجة الأخطاء، ومكان واحد للتعامل مع الاستجابات الفارغة. أي تطبيق جدّي لا بدّ أن ينتهي بشيء مشابه لهذا.
الخطوة التالية: معالجة الأخطاء في الكود غير المتزامن
يُعدّ fetch من أكثر الأماكن التي تظهر فيها أخطاء الكود غير المتزامن، وفحص response.ok ما هو إلا قطعة واحدة من الصورة الكاملة. الصفحة التالية تتناول معالجة الأخطاء في الـ Promises وasync/await — أين تذهب الأخطاء، وكيف تلتقطها، وما هي الفخاخ التي تجعلها تمرّ دون أن تلاحظها.
الأسئلة الشائعة
كيف أستخدم fetch في جافا سكريبت؟
تستدعي fetch(url) ومررها الرابط المطلوب، وهي تُرجع Promise يُحَلّ إلى كائن Response. بعدها تستدعي response.json() (وهي أيضًا تُرجع promise) لقراءة محتوى الـ body. مع async/await يصير الكود أبسط: const res = await fetch(url); const data = await res.json();.
كيف أرسل طلب POST باستخدام fetch؟
مرّر وسيطًا ثانيًا يحتوي على method: 'POST'، وكائن headers (عادةً 'Content-Type': 'application/json')، وbody — ولا تنسَ تحويل الكائن إلى نص عبر JSON.stringify(...). لأن fetch لن يقوم بتحويل الـ body نيابةً عنك.
لماذا لا يفشل fetch عند الأخطاء 404 أو 500؟
لأن fetch يرفض الوعد فقط عند أخطاء الشبكة — مثل فشل DNS أو انقطاع الاتصال أو حجب CORS. أما أكواد HTTP الخاطئة فتُعتبر استجابات ناجحة بالنسبة للـ promise. لذا يجب عليك التحقق يدويًا من response.ok (تكون true للأكواد 200–299) أو من response.status، ثم ترمي خطأ بنفسك إذا رجع الخادم باستجابة غير سليمة.
هل يمكنني إلغاء طلب fetch أثناء تنفيذه؟
نعم، عبر AbortController. أنشئ نسخة منه، ومرّر signal الخاصة به إلى fetch ضمن كائن الخيارات، ثم استدعِ controller.abort() وقت ما تريد الإلغاء. عندها سيرفض الـ promise الخاص بـ fetch مع خطأ AbortError يمكنك التقاطه داخل catch.