Menu
CoddyTech

معالجة الأخطاء

جزء من قسم أساسيات 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 عندما يرسل الزائر شيئًا خاطئًا يمكنه إصلاحه. ألقِ خطأً عندما يكون الخادم هو المسؤول.

challenge icon

التحدي

سهل

تابع العمل على Recipe Box. يمكنك تجاهل ملفات practice-errors/ المنفصلة؛ إذ يبدأ زر التشغيل الآن خادم المشروع server.js.

يريد الطهاة زيادة مقادير وصفة، مثلًا مضاعفة عدد الحصص لحفلة. لا يمكن زيادة مقادير وصفة محفوظة من دون عدد للحصص. هذا خلل في بياناتك، وليس أمرًا يستطيع الزائر إصلاحه، لذا يُرسل إلى معالج أخطاء يُرجع استجابة JSON نظيفة برمز الحالة 500.

  1. في server.js، أضف مسارًا لـ /api/recipes/:id/scale فوق معالج 404. يقرأ هذا المسار معامل الزيادة من سلسلة الاستعلام، كما في /api/recipes/1/scale?by=2.
  2. إذا لم تكن هناك وصفة بهذا المعرّف، فأرسل استجابة برمز الحالة 404 و {"error": "No such recipe"}.
  3. حوّل by إلى رقم. إذا لم يكن رقمًا، أو كان يساوي 0 أو أقل، فأرسل استجابة برمز الحالة 400 و {"error": "Scale by a positive number"}. تعطي Number('lots') القيمة NaN، وتكتشفها Number.isNaN.
  4. إذا لم يكن للوصفة servings، فألقِ خطأً جديدًا من Error تكون رسالته Recipe 5 has no servings، مع وضع معرّف تلك الوصفة بدلًا من 5.
  5. وإلا، فأرسل استجابة JSON لا تحتوي إلا على العنوان وعدد الحصص بعد زيادة المقادير. تحتوي الوصفة 1، Tomato Soup، على 4 حصص، لذا يرسل /api/recipes/1/scale?by=2 القيمة {"title": "Tomato Soup", "servings": 8}. لا تضف أي حقول أخرى.
  6. في نهاية سلسلة البرمجيات الوسيطة في الملف، بعد معالج 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');
});
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس أساسيات Express

تدرّب بنفسك: Playground لـ Express