معالجة الأخطاء
جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 18 من 23.
عندما يُلقي المسار خطأً، يرسل Express صفحة الخطأ الخاصة به، وهي ليست بتنسيق JSON وتعرض تفاصيل مخصّصة لك.
app.get('/api/fines/:member', (req, res) => {
throw new Error('Fines database is down');
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Try again later' });
});البرمجية الوسيطة التي تتضمّن أربعة وسيطات، (err, req, res, next)، هي معالج أخطاء. يحصي Express الوسيطات: بثلاثة وسيطات تكون برمجية وسيطة عادية، وتتجاوزها الأخطاء. عندما يُلقي المسار خطأً، يتجاوز Express البرمجيات الوسيطة العادية ويستدعي معالج الأخطاء مع الخطأ. أضِفه أخيرًا، بعد معالج 404. سجّل التفاصيل لنفسك، وأرسل إلى العميل رسالة موجزة.
تُوقِف throw new Error('text') المسار وتُمرّر الخطأ إلى Express، كما أن استدعاء next(err) يفعل الشيء نفسه. أرسِل استجابة 400 عندما يرسل الزائر شيئًا خاطئًا يمكنه إصلاحه. ألقِ خطأً عندما يكون الخادم هو المسؤول.
التحدي
سهلتابع العمل على Recipe Box. يمكنك تجاهل ملفات practice-errors/ المنفصلة؛ إذ يبدأ زر التشغيل الآن خادم المشروع server.js.
يريد الطهاة زيادة مقادير وصفة، مثلًا مضاعفة عدد الحصص لحفلة. لا يمكن زيادة مقادير وصفة محفوظة من دون عدد للحصص. هذا خلل في بياناتك، وليس أمرًا يستطيع الزائر إصلاحه، لذا يُرسل إلى معالج أخطاء يُرجع استجابة JSON نظيفة برمز الحالة 500.
- في
server.js، أضف مسارًا لـ/api/recipes/:id/scaleفوق معالج 404. يقرأ هذا المسار معامل الزيادة من سلسلة الاستعلام، كما في/api/recipes/1/scale?by=2. - إذا لم تكن هناك وصفة بهذا المعرّف، فأرسل استجابة برمز الحالة 404 و
{"error": "No such recipe"}. - حوّل
byإلى رقم. إذا لم يكن رقمًا، أو كان يساوي 0 أو أقل، فأرسل استجابة برمز الحالة 400 و{"error": "Scale by a positive number"}. تعطيNumber('lots')القيمةNaN، وتكتشفهاNumber.isNaN. - إذا لم يكن للوصفة
servings، فألقِ خطأً جديدًا منErrorتكون رسالتهRecipe 5 has no servings، مع وضع معرّف تلك الوصفة بدلًا من 5. - وإلا، فأرسل استجابة JSON لا تحتوي إلا على العنوان وعدد الحصص بعد زيادة المقادير. تحتوي الوصفة 1، Tomato Soup، على 4 حصص، لذا يرسل
/api/recipes/1/scale?by=2القيمة{"title": "Tomato Soup", "servings": 8}. لا تضف أي حقول أخرى. - في نهاية سلسلة البرمجيات الوسيطة في الملف، بعد معالج 404، أضف معالج أخطاء: استدعاء
app.useتكون دالته ذات أربع وسيطات،(err, req, res, next). يطبع الخطأ باستخدامconsole.errorويرسل استجابة برمز الحالة 500 و{"error": "Something went wrong"}.
جرّب ذلك: اضغط على زر التشغيل وأرسل /api/recipes/1/scale?by=2. ينبغي أن ترى {"title":"Tomato Soup","servings":8}. أرسل /api/recipes/2/scale?by=lots: رمز الحالة 400 و {"error":"Scale by a positive number"}. أرسل /api/recipes/99/scale?by=2: رمز الحالة 404. اختر الآن POST وأرسل {"title": "Toast"} إلى /api/recipes. ستصبح الوصفة رقم 5، من دون عدد للحصص. أرسل /api/recipes/5/scale?by=2: رمز الحالة 500 و {"error":"Something went wrong"}، وستعرض وحدة التحكم الخطأ Recipe 5 has no servings.
جرّب بنفسك
import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
const app = express();
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.use((req, res, next) => {
res.set('X-Request-Id', randomUUID());
next();
});
app.use(express.json());
app.use(express.static('public'));
app.get('/', (req, res) => {
const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});
// ترسل type('text') نصًا عاديًا، لذا يظهر كل عنوان في سطر مستقل.
app.get('/recipes', (req, res) => {
const { tag } = req.query;
const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});
app.get('/recipes/:id', (req, res) => {
const recipe = recipes.find((r) => r.id === Number(req.params.id));
if (!recipe) {
res.status(404).send('No such recipe');
return;
}
res.send(recipe.title);
});
app.get('/api/recipes', (req, res) => {
res.json(recipes);
});
app.get('/api/recipes/:id', (req, res) => {
const recipe = recipes.find((r) => r.id === Number(req.params.id));
if (!recipe) {
res.status(404).json({ error: 'No such recipe' });
return;
}
res.json(recipe);
});
app.post('/api/recipes', (req, res) => {
const { title, tags } = req.body;
if (!title) {
res.status(400).json({ error: 'A recipe needs a title' });
return;
}
const recipe = { id: recipes.length + 1, title, tags: tags ?? [] };
recipes.push(recipe);
res.status(201).json(recipe);
});
app.get('/about', (req, res) => {
res.send('Recipe Box collects family recipes.');
});
app.get('/menu', (req, res) => {
res.redirect(301, '/recipes');
});
app.get('/menu/:id', (req, res) => {
res.redirect(301, `/recipes/${req.params.id}`);
});
app.use((req, res) => {
if (req.path.startsWith('/api/')) {
res.status(404).json({ error: 'Not found' });
return;
}
res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});
app.listen(3000, () => {
console.log('Recipe Box is running on port 3000');
});
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس أساسيات Express
تدرّب بنفسك: Playground لـ Express