Node.js / الأساسي: بيئة تشغيل Node.js / الدرس 5
طلبات HTTP واستجاباتها
استخدم HTTP المدمج لتمييز الطلبات وإرسال استجابات كاملة واختبار خادم دون تركه يعمل.
في هذه الصفحة
أهداف الدرس
- اشرح المفاهيم والمفاضلات المتعلقة بموضوع «طلبات HTTP واستجاباتها».
- توقع المثالين وتحقق من مخرجاتهما وسلوك تنظيف الموارد فيهما.
- شخّص الإخفاقات الثلاثة الشائعة وطبق الحلول المشروحة على مدخلات مشابهة.
HTTP بروتوكول تطبيق
يحمل طلب HTTP طريقة وهدفا وترويسات وأحيانا جسما. وتحمل الاستجابة حالة وترويسات وجسما اختياريا. قد ينقل اتصال TCP أكثر من طلب، لذا تختلف حياة الاتصال عن حياة الطلب. تُستخدم GET عادة لاسترجاع تمثيل وPOST لمعالجة محتوى مرسل. يجب أن يحدد التطبيق سلوك كل مسار؛ فالطريقة ليست إذنا للوصول. تضيف HTTPS تشفير النقل والتحقق من الطرف الآخر. يناسب HTTP المحلي التعلم، لكنه لا يغني عن تصميم TLS عند النشر.
تحليل الهدف واختيار المسار
تعرض HTTP المدمجة req.url بوصفه هدف الطلب، لا رابطا عاما موثوقا كاملا. استخدم قاعدة ثابتة مع URL لتحليل المسار والاستعلام. لا تبن روابط حساسة أمنيا من ترويسة Host غير الموثوقة. طابق الطريقة وpathname معا، ولا تجعل الاستعلام جزءا من اسم المسار عرضا. قرر متى تعيد 404 ومتى تعيد 405 لمسار معروف بطريقة مختلفة. أبق قرارات التوجيه واضحة قابلة للمراجعة قبل إضافة تجريدات إطار عمل.
إكمال استجابة واحدة بالضبط
يعيد الخادم الأول JSON عند GET /health فقط، ويعيد 404 نصية لبقية الحالات. تختار writeHead الحالة والترويسات، وتنهي end الاستجابة. ارجع مباشرة بعد إكمال الفرع كي لا يتابع التنفيذ إلى استجابة ثانية. بعد إرسال الترويسات لا يستطيع استثناء لاحق استبدال الاستجابة بأمان بخطأ JSON جديد. يحتاج المعالج الإنتاجي إلى حد واضح للأخطاء وسياسة للاستجابات الجزئية. الطلبات هنا صغيرة، ويستهلك العميل الجسم قبل إغلاق الخادم.
import { createServer } from 'node:http';
import { once } from 'node:events';
const server = createServer((req, res) => {
const url = new URL(req.url, 'http://local');
if (req.method === 'GET' && url.pathname === '/health') {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ ok: true }));
}
res.writeHead(404); res.end('Not found');
});
server.listen(0, '127.0.0.1'); await once(server, 'listening');
try {
const response = await fetch(`http://127.0.0.1:${server.address().port}/health`);
console.log(response.status, (await response.json()).ok);
} finally { await new Promise((resolve, reject) => server.close(e => e ? reject(e) : resolve())); }
200 true
يحلل العميل JSON بعد تلقي الاستجابة. وتغلق finally الاستماع حتى إذا فشل تحقق العميل.
التمثيلات وطريقة HEAD
توضح Content-Type كيفية تفسير الجسم، وينبغي وسم JSON بـ application/json. تعيد HEAD ترويسات التمثيل المختار دون جسمه. يستخدم المثال الثاني Buffer.byteLength لأن عدد بايتات النص قد يختلف عن عدد محارف JavaScript. تصف Content-Length التمثيل الذي كانت GET سترسله، بينما تنقل HEAD صفرا من بايتات الجسم. تجنب تحديد الطول يدويا عندما تغيره طبقة ضغط أو تحويل تدفق. دع طبقة النقل المناسبة تدير التأطير إذا كان الحجم النهائي مجهولا.
import { createServer } from 'node:http';
import { once } from 'node:events';
const body = 'hello';
const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain', 'content-length': Buffer.byteLength(body) });
res.end(req.method === 'HEAD' ? undefined : body);
});
server.listen(0, '127.0.0.1'); await once(server, 'listening');
try {
const response = await fetch(`http://127.0.0.1:${server.address().port}`, { method: 'HEAD' });
console.log(response.status, response.headers.get('content-length'), (await response.text()).length);
} finally { await new Promise((resolve, reject) => server.close(e => e ? reject(e) : resolve())); }
200 5 0
تعلن HEAD تمثيلا من خمسة بايتات لكنها لا ترسل جسما، فيكون طول النص المستلم صفرا.
امتلاك دورة حياة الخادم
يستمع المثالان على 127.0.0.1 بالمنفذ صفر ليختار النظام منفذا مؤقتا متاحا. يحصلان على المنفذ الفعلي بعد نجاح الاستماع، ويغلقان الخادم داخل finally. يمنع ذلك حجز منفذ ثابت أو بقاء العرض حيا بعد الاختبار. وتحتاج التطبيقات الطويلة إلى معالجة أحداث أخطاء الخادم أيضا. حدد مهلا مناسبة للطلبات والترويسات، وقيد الأجسام، وفكر في وكيل عكسي عند النشر. يسهّل فهم دورة الطلب الأساسية اتخاذ هذه القرارات حتى مع استخدام إطار للتوجيه.
الأخطاء الشائعة وإصلاحها
ERR_HTTP_HEADERS_SENT
- لماذا يحدث؟
- حاول فرع ضبط الترويسات بعد استجابة فرع آخر.
- كيف تصلحه؟
- ارجع بعد end واجعل جهة واحدة تتولى الاستجابة.
- كيف تتجنب تكراره؟
- اختبر كل فروع المعالج بما فيها الفشل.
طلب لا ينتهي
- لماذا يحدث؟
- كتب المعالج الترويسات ولم ينه الاستجابة.
- كيف تصلحه؟
- استدع end أو أنه تدفقا مملوكا في كل مسار مكتمل.
- كيف تتجنب تكراره؟
- ضع مهلا لاختبارات التكامل وتحقق من التنظيف.
EADDRINUSE
- لماذا يحدث؟
- المنفذ الثابت مستخدم بالفعل.
- كيف تصلحه؟
- استخدم صفرا في الاختبارات أو اضبط منفذا متاحا للتطبيق.
- كيف تتجنب تكراره؟
- أغلق خوادم الاختبار داخل finally.
اختبر فهمك
1. يستهدف طلب GET المسار /health?verbose=1. أي قيمة يفحصها التوجيه؟
اعرض الحل المشروح
حلل الهدف بقاعدة ثابتة وافحص url.pathname بقيمة /health، ثم افحص searchParams للاستعلام verbose بصورة مستقلة. طابق GET أيضا. ستؤدي مقارنة الهدف الخام بـ /health إلى رفض طلب صالح يحتوي استعلاما.
2. يرسل معالج استجابة 400 ثم يتابع إلى فرع النجاح. أصلح تدفقه.
اعرض الحل المشروح
ارجع مباشرة من فرع الخطأ بعد res.end. يجب ألا يصل التنفيذ إلى النجاح إلا بعد اجتياز التحقق. اختبر طلبا غير صالح وتحقق من حالة واحدة وجسم واحد؛ فاختبار المسار الصحيح وحده لا يكشف عيب الاستجابة المكررة.
أهم ما تعلمته
- يحمل طلب HTTP طريقة وهدفا وترويسات وأحيانا جسما.
- تعرض HTTP المدمجة req.url بوصفه هدف الطلب، لا رابطا عاما موثوقا كاملا.
- يستمع المثالان على 127.0.0.1 بالمنفذ صفر ليختار النظام منفذا مؤقتا متاحا.