Kodokon kodokon.com

การตรวจสอบข้อมูลนำเข้าและ error middleware

ตรวจสอบข้อมูลตั้งแต่ขอบของ API และรวมศูนย์การจัดการข้อผิดพลาดทั้งหมดไว้ใน middleware สี่พารามิเตอร์ตัวเดียว

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

ข้อมูลขาเข้าทุกชิ้นถือเป็นศัตรูจนกว่าจะพิสูจน์ได้เป็นอย่างอื่น ไม่ว่าจะเป็น JSON body, พารามิเตอร์ URL หรือ header การตรวจสอบควรอยู่ที่ ขอบ ของแอปพลิเคชัน คือก่อนถึง controller เพื่อให้ชั้นด้านในทำงานกับข้อมูลที่รับประกันแล้ว จากนั้นจงแยกแยะข้อผิดพลาดสองตระกูล คือ ข้อผิดพลาด เชิงปฏิบัติการ (operational) (ข้อมูลนำเข้าไม่ถูกต้อง, ไม่พบทรัพยากร, ความขัดแย้ง) ซึ่งคาดการณ์ได้และถูกแปลงเป็น 4xx และข้อผิดพลาด เชิงการเขียนโปรแกรม (programming) (บั๊ก) ซึ่งต้องให้ผลเป็น 500 ที่ทึบต่อฝั่งไคลเอนต์ และ stack trace เต็มรูปแบบที่ฝั่งเซิร์ฟเวอร์

JAVASCRIPT
export class HttpError extends Error {
  constructor(status, message, details = undefined) {
    super(message);
    this.status = status;
    this.details = details;
  }
}
ข้อผิดพลาดที่พกพารหัสสถานะ HTTP ของมันไปด้วย คือหน่วยประกอบสร้างพื้นฐาน

middleware ตรวจสอบจะดักคำขอไว้ก่อนถึง controller เวอร์ชันเขียนเองด้านล่างแสดงหลักการ ส่วนใน production สคีมาแบบประกาศ (zod ได้กลายเป็นมาตรฐาน) จะให้การอนุมานชนิด (type inference) และข้อความที่มีโครงสร้างเพิ่มเข้ามาด้วย การแลกเปลี่ยนคือ การพึ่งพาหนึ่งตัวและต้นทุนตอนรันไทม์เล็กน้อย แลกกับกฎที่รวมศูนย์ ประกอบเข้าด้วยกันได้ และเป็นไปไม่ได้ที่จะลืม

JAVASCRIPT
import { HttpError } from './http-error.js';

export function validateUser(req, res, next) {
  const { email, name } = req.body ?? {};
  const errors = [];
  if (typeof email !== 'string' || !email.includes('@')) {
    errors.push('email: invalid format');
  }
  if (typeof name !== 'string' || name.trim().length < 2) {
    errors.push('name: 2 characters minimum');
  }
  if (errors.length > 0) {
    return next(new HttpError(400, 'Invalid body', errors));
  }
  next();
}
เชื่อมมันเข้าด้วย router.post('/', validateUser, controller.create)

Express รู้จัก error middleware จาก ลายเซ็นสี่พารามิเตอร์ (err, req, res, next) ของมัน เมื่อลงทะเบียนไว้ท้ายสุด หลังจาก route ทั้งหมด ด้วย app.use(errorHandler) มันจะกลายเป็นทางออกข้อผิดพลาดเพียงทางเดียวของ API คือ รูปแบบคำตอบที่เป็นเนื้อเดียวกัน การบันทึก log ที่จุดเดียว และไม่มีการรั่วไหลของข้อมูลภายใน ส่วน controller นั้นก็เพียงแค่เรียก next(err)

JAVASCRIPT
export function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  const status = err.status ?? 500;
  const body = { error: err.message };
  if (err.details) {
    body.details = err.details;
  }
  if (status >= 500) {
    console.error(err);
    body.error = 'Internal server error';
  }
  res.status(status).json(body);
}
4xx โปร่งใสต่อไคลเอนต์ ส่วน 500 ถูกปิดบังแต่ถูกบันทึก log ไว้

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. Express แยกแยะ error middleware ออกจาก middleware ธรรมดาได้อย่างไร?
    • จากชื่อของมัน ซึ่งต้องมีคำว่า error
    • จากลายเซ็นสี่พารามิเตอร์ (err, req, res, next) ของมัน
    • จากตำแหน่งที่อยู่แรกสุดในสายโซ่
    • จากการเรียก app.setErrorHandler อย่างชัดแจ้ง
  2. คำตอบ HTTP ที่ถูกต้องสำหรับข้อผิดพลาดเชิงการเขียนโปรแกรมที่ไม่คาดคิดคืออะไร?
    • 500 พร้อมข้อความและ stack เพื่อช่วยในการวินิจฉัย
    • 400 เพื่อส่งสัญญาณว่าคำขอล้มเหลว
    • 500 พร้อมข้อความทั่วไป โดยข้อผิดพลาดเต็มรูปแบบถูกบันทึก log ไว้ที่ฝั่งเซิร์ฟเวอร์
  3. ใน Express 4 จะเกิดอะไรขึ้นถ้า handler แบบอะซิงโครนัสถูก reject โดยไม่มี try/catch?
    • ข้อผิดพลาดยังคงไปถึง error middleware
    • คำขอจะค้างอยู่ การ reject ไม่เคยถูกส่งต่อไปยัง next
    • Express คืนค่า 400 โดยอัตโนมัติ
    • โปรเซส Node หยุดทำงานทันที