10 KiB
รายงานการวิเคราะห์จุดเสี่ยง Unhandled Exception - Controllers ชุดที่ 6 (51-60)
วันที่วิเคราะห์: 2026-05-08
สรุปผลการวิเคราะห์
จากการตรวจสอบ Controllers ทั้ง 10 ไฟล์ (51-60):
- ProfileAbilityEmployeeController
- ProfileAbilityEmployeeTempController
- ProfileAbsentLateController
- ProfileActpositionController
- ProfileActpositionEmployeeController
- ProfileActpositionEmployeeTempController
- ProfileAddressController
- ProfileAddressEmployeeController
- ProfileAddressEmployeeTempController
- ProfileAssessmentsController
พบ 0 จุดเสี่ยงระดับวิกฤต ที่อาจทำให้เกิด Unhandled Exception และ Crash Loop ในระบบ Microservices
รายละเอียดจุดเสี่ยงที่พบ
ไม่พบจุดเสี่ยงระดับวิกฤต
Controllers ทั้งหมดในชุดนี้มีการจัดการ Error ที่ดี โดย:
- ทุก Method ใช้ async/await อย่างถูกต้อง - ไม่มี Promise ที่ถูกเรียกโดยไม่มี await
- มีการ throw HttpError - เมื่อเกิด Error จะ throw HttpError ที่มี Status Code ที่ชัดเจน
- Database Operations ล้วนอยู่ใน try-catch โดยนัย - TypeORM repositories มีการ handle error ภายใน
- ใช้ Promise.all อย่างปลอดภัย - ใน operations ที่ต้องบันทึกข้อมูลหลายจุดพร้อมกัน
จุดที่ควรปรับปรุง (แนะนำ)
แม้จะไม่พบจุดเสี่ยงระดับวิกฤต แต่มีจุดที่ควรปรับปรุงเพื่อเพิ่มความแข็งแกร่งของระบบ:
1. File: ProfileAbilityEmployeeController.ts, ProfileAbilityEmployeeTempController.ts, ProfileActpositionEmployeeController.ts, ProfileActpositionEmployeeTempController.ts
Method: detailProfileAbilityUser, detailProfileActpositionUser
Problem Type: 2. Missing Error Handle (Potential Null Reference)
Root Cause:
// Lines 42-48
const getProfileAbilityId = await this.profileAbilityRepo.find({
where: { profileEmployeeId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (!getProfileAbilityId) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
find() method จะ return empty array [] เมื่อไม่พบข้อมูล ไม่ใช่ null หรือ undefined ดังนั้น condition !getProfileAbilityId จะไม่เคยเป็น true
Recommended Fix:
const getProfileAbilityId = await this.profileAbilityRepo.find({
where: { profileEmployeeId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (getProfileAbilityId.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// หรือถ้าต้องการให้ return empty array ได้
return new HttpSuccess(getProfileAbilityId);
2. File: ProfileAbsentLateController.ts
Method: newAbsentLateBatch
Problem Type: 2. Missing Error Handle (Transaction Safety)
Root Cause:
// Lines 159-168
const result = await this.absentLateRepo.save(records, { data: req });
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
ถ้าการบันทึก history ล้มเหลว ข้อมูลหลัก (records) จะถูกบันทึกไปแล้ว ทำให้เกิด Data Inconsistency
Recommended Fix:
// ใช้ Transaction หรือ wrap ด้วย try-catch
try {
const result = await this.absentLateRepo.save(records, { data: req });
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
} catch (error) {
// ถ้าเกิด error ควร rollback หรือลบข้อมูลที่บันทึกไปแล้ว
// หรือใช้ Transaction ของ TypeORM
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการบันทึกข้อมูล");
}
3. File: ProfileActpositionController.ts
Method: getProfileActpositionHistory
Problem Type: 2. Missing Error Handle (Potential Null Reference in Relations)
Root Cause:
// Lines 95-104
const record = await this.profileActpositionHistoryRepo.find({
relations: ["histories"],
where: { profileActpositionId: actpositionId },
order: { createdAt: "DESC" },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const mappedRecords = record.map(history => {
const firstHistory = history.histories ?? [];
มีการใช้ relations: ["histories"] แต่ไม่มีการตรวจสอบว่า relation นี้มีอยู่จริงใน Entity หรือไม่ ถ้า relation ไม่ถูกต้องอาจเกิด error
Recommended Fix:
try {
const record = await this.profileActpositionHistoryRepo.find({
relations: ["histories"],
where: { profileActpositionId: actpositionId },
order: { createdAt: "DESC" },
});
if (record.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const mappedRecords = record.map(history => {
const firstHistory = Array.isArray(history.histories) ? history.histories[0] : null;
return {
// ... rest of mapping
};
});
return new HttpSuccess(mappedRecords);
} catch (error) {
if (error instanceof HttpError) throw error;
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการดึงข้อมูล");
}
4. All Controllers
Method: ทุก Method ที่ใช้ Promise.all
Problem Type: 2. Missing Error Handle (Partial Failure)
Root Cause:
// Pattern ที่ใช้ในหลาย ๆ Controller
await Promise.all([
this.profileRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
ถ้า setLogDataDiff หรือ historyRepo.save ล้มเหลว แต่ profileRepo.save สำเร็จ จะเกิด Data Inconsistency
Recommended Fix:
// ใช้ Transaction ของ TypeORM แทน
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(Profile, record);
await transactionalEntityManager.save(ProfileHistory, history);
// setLogDataDiff ควรอยู่นอก transaction หรือ handle error แยก
});
setLogDataDiff(req, { before, after: record });
สรุปคำแนะนำการแก้ไข
ระดับความสำคัญ: สูง
- ใช้ Transaction สำหรับ Operations ที่ต้องบันทึกข้อมูลหลายตาราง - เพื่อป้องกัน Data Inconsistency
- ตรวจสอบค่าที่ return จาก
find()อย่างถูกต้อง - ใช้.length === 0แทน!result
ระดับความสำคัญ: ปานกลาง
- เพิ่ม Error Boundary หรือ Global Error Handler - เพื่อจัดการ error ที่ไม่คาดคิด
- Log error ที่เกิดขึ้น - เพื่อช่วยในการ Debug และ Monitor
ระดับความสำคัญ: ต่ำ
- Refactor code ให้ใช้ Transaction Manager - เพื่อให้ code สะอาดและปลอดภัยมากขึ้น
การจัดการ Error ที่ดีที่สุดสำหรับ Microservices
// 1. ใช้ AsyncHandler Wrapper
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 2. ใช้ Global Error Handler
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});
// 3. ใช้ Transaction สำหรับ Database Operations
await AppDataSource.transaction(async (manager) => {
// All database operations here
});
สรุป
Controllers ในชุดที่ 6 (51-60) มีความเสี่ยงต่ำต่อการเกิด Unhandled Exception ที่จะทำให้ Service Crash แต่มีจุดที่ควรปรับปรุงเพื่อ:
- ป้องกัน Data Inconsistency - โดยการใช้ Transaction
- ปรับปรุง Logic การตรวจสอบข้อมูล - โดยการเช็ค length ของ array ที่ return จาก find()
- เพิ่มความแข็งแกร่งของระบบ - โดยการเพิ่ม Error Handling และ Logging
ไม่มีจุดเสี่ยงระดับวิกฤตที่จะทำให้เกิด Crash Loop ในทันที แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว