معالجة الأخطاء
جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 15 من 20.
عندما يُصدر أحد المسارات استثناءً، يعرض 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. سجّل التفاصيل لنفسك؛ وأرسل إلى العميل رسالة قصيرة.
القيمة غير الصالحة في ?by= هي خطأ الزائر، لذا تظل الاستجابة له برمز 400. أما الخطأ فيتعلق بشيء لا يستطيع الزائر إصلاحه، مثل وصفة حُفظت من دون عدد حصصها.
التحدي
سهل- يستجيب
GET /api/recipes/:id/scale?by=2بـ{ "title", "servings" }، مع ضرب عدد الحصص. - إذا لم يكن العامل عددًا موجبًا:
400مع{ "error": "Scale by a positive number" }. - الوصفة التي لا تحتوي على
servingsتُلقيRecipe <id> has no servings. - يسجل معالج الأخطاء الخطأ ويستجيب بـ
500مع{ "error": "Something went wrong" }.
أرسل ?by=2 و?by=lots. ثم أرسل { "title": "Toast" } إلى /api/recipes واضرب مقادير الوصفة رقم 5.
جرّب بنفسك
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'));
const escapeHtml = (text) =>
text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
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');
});
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.