14 KiB
14 KiB
Batch 08: Controllers 71-80 Analysis - Unhandled Exception & Crash Loop Risks
Executive Summary
พบจุดเสี่ยงระดับ CRITICAL ที่อาจทำให้เกิด Unhandled Exception และ Crash Loop ในระบบ Microservices จำนวน 8 จุด จากการตรวจสอบ 10 Controllers ในชุดที่ 8
Critical Issues Found
1. CRITICAL - Unhandled External API Call in ProfileController.ts
File & Location
- File:
src/controllers/ProfileController.ts - Methods:
- Line 484-499:
getSalaryProfile()method - Line 977-992: Similar pattern in another method
- Line 484-499:
Problem Type
- Unhandled Exception
- Silent Error Swallowing
Root Cause
// Line 484-499
await Promise.all(
await profiles.profileAvatars.slice(-7).map(async (x, i) => {
if (x == null) {
_ImgUrl[i] = null;
} else {
const url = process.env.API_URL + `/salary/file/${x?.avatar}/${x?.avatarName}`;
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch {} // ❌ SILENT ERROR - Empty catch block
}
})
);
รายละเอียดปัญหา:
- Empty catch block: มีการใช้
catch {}ว่างเปล่า ทำให้ไม่ทราบว่าเกิด Error 什么 - Unhandled Promise rejection: หาก axios.get throw exception ภายใน Promise.all อาจทำให้เกิด Unhandled Promise Rejection
- External API dependency: เรียก API ภายนอก (API_URL) โดยไม่มี Timeout handling
- No retry logic: ไม่มีการ retry เมื่อเกิด Error
ผลกระทบ:
- หาก External API ล่มหรือ Timeout อาจทำให้ Request ค้างอยู่นาน
- ไม่มี Logging ทำให้ยากต่อการ Debug
- อาจทำให้ Memory Leak หาก Promise ไม่ resolve
2. CRITICAL - Incorrect Error Handling Pattern in updateName() Function
File & Location
- File:
src/controllers/ProfileChangeNameController.ts- Lines 118-128:
newChangeName()method - Lines 189-200:
editChangeName()method
- Lines 118-128:
- File:
src/controllers/ProfileChangeNameEmployeeController.ts- Lines 124-134:
newChangeName()method - Lines 189-200:
editChangeName()method (similar pattern)
- Lines 124-134:
- File:
src/controllers/ProfileChangeNameEmployeeTempController.ts- Lines 116-126:
newChangeName()method
- Lines 116-126:
- File:
src/controllers/ProfileController.ts- Lines 5473-5483: Update profile method
- Lines 5792-5802: Update profile method
Problem Type
- Unhandled Exception
- Type Error Risk
Root Cause
// Pattern found across multiple controllers
if (profile != null && profile.keycloak != null && profile.isDelete === false) {
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
if (!result) {
throw new Error(result.errorMessage); // ❌ CRITICAL BUG
}
}
รายละเอียดปัญหา:
- Accessing property of undefined: เมื่อ
resultเป็นfalse(falsy value) การพยายามเข้าถึงresult.errorMessageจะทำให้เกิด TypeError - Unhandled Exception: TypeError นี้จะไม่ถูก catch และจะ propagate ขึ้นไปทำให้ Service Crash
- Inconsistent return type: ฟังก์ชัน
updateName()ในsrc/keycloak/index.tsส่งค่ากลับเป็นfalse,true,id, หรือobject with errorMessage(ไม่ consistent)
ตรวจสอบฟังก์ชัน updateName():
// src/keycloak/index.ts:525-533
if (!res) return false;
if (!res.ok) {
return await res.json(); // Returns error object with errorMessage
}
const path = res.headers.get("Location");
const id = path?.split("/").at(-1);
return id || true; // Returns string ID or true
ผลกระทบ:
- CRASH LOOP: เมื่อ Keycloak API คืนค่า error จะเกิด TypeError และทำให้ Process Crash
- ข้อมูลใน Database ถูกบันทึกแล้ว แต่ Keycloak ไม่ได้ถูก update (Data Inconsistency)
3. HIGH - Missing Error Handling in Promise.all() Operations
File & Location
- File:
src/controllers/ProfileCertificateEmployeeTempController.ts- Lines 155-163:
editCertificate()method
- Lines 155-163:
- File:
src/controllers/ProfileDevelopmentController.ts- Lines 294-297:
editDevelopment()method
- Lines 294-297:
- File:
src/controllers/ProfileDevelopmentEmployeeController.ts- Lines 237-240:
editDevelopment()method
- Lines 237-240:
Problem Type
- Missing Error Handle
- Data Consistency Risk
Root Cause
// Example from ProfileCertificateEmployeeTempController.ts:155-163
await Promise.all([
this.certificateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.certificateHistoryRepository.save(history, { data: req }),
]);
รายละเอียดปัญหา:
- Partial failure risk: หาก
setLogDataDiff()throw error การ save ทั้ง 2 จุดก่อนหน้านี้จะเสียไป - No transaction: ไม่มีการใช้ Transaction ในการ save ข้อมูลหลายตาราง
- Orphaned data: อาจเกิดข้อมูลปนกันระหว่าง production และ history
4. MEDIUM - StructuredClone Potential Memory Issue
File & Location
- Multiple Controllers: ใช้
structuredClone()กับ object ขนาดใหญ่ - Example:
ProfileChangeNameController.ts:137,ProfileDevelopmentController.ts:349
Problem Type
- Memory Issue
- Performance Risk
Root Cause
const before = structuredClone(record); // record อาจมีขนาดใหญ่
รายละเอียดปัญหา:
structuredClone()ใช้เวลาและ memory มากกับ object ขนาดใหญ่- อาจทำให้เกิด Memory Heap Overflow ใน Production
Recommended Fixes
Fix 1: ProfileController.ts - External API Call with Proper Error Handling
Before:
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch {} // ❌ Empty catch
After:
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
timeout: 5000, // Add timeout
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch (error) {
console.error(`Failed to fetch avatar ${x?.avatar}:`, error.message);
_ImgUrl[i] = null; // Fallback to null
// Or re-throw if critical: throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "Avatar service unavailable");
}
Fix 2: Incorrect Error Handling Pattern - ALL Controllers
Before:
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
if (!result) {
throw new Error(result.errorMessage); // ❌ TypeError when result is false
}
After:
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
// Check result type properly
if (result === false || (result && result.errorMessage)) {
const errorMessage = result?.errorMessage || 'Failed to update name in Keycloak';
console.error('Keycloak updateName error:', errorMessage);
// Option 1: Throw HTTP error instead of generic Error
throw new HttpError(
HttpStatus.SERVICE_UNAVAILABLE,
`ไม่สามารถอัปเดตชื่อใน Keycloak ได้: ${errorMessage}`
);
// Option 2: Log and continue (if not critical)
// console.warn(`Keycloak update failed for user ${profile.keycloak}: ${errorMessage}`);
// Don't throw - just log the error
}
OR Fix the keycloak function to return consistent type:
// src/keycloak/index.ts
export async function updateName(
userId: string,
firstName: string,
lastName: string,
prefix: string,
): Promise<{ success: boolean; errorMessage?: string }> {
try {
const existingUser = await getUser(userId);
if (!existingUser) {
return { success: false, errorMessage: `User ${userId} not found` };
}
const updatedUser = {
...existingUser,
firstName,
lastName,
attributes: {
...(existingUser.attributes || {}),
prefix,
},
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify(updatedUser),
});
if (!res.ok) {
const errorData = await res.json();
return { success: false, errorMessage: errorData.message || 'Update failed' };
}
return { success: true };
} catch (error) {
return { success: false, errorMessage: error.message };
}
}
Fix 3: Add Transaction Support for Multi-Table Operations
Before:
await Promise.all([
this.certificateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.certificateHistoryRepository.save(history, { data: req }),
]);
After:
try {
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(ProfileCertificate, record);
await transactionalEntityManager.save(ProfileCertificateHistory, history);
});
// Log diff outside transaction
setLogDataDiff(req, { before, after: record });
} catch (error) {
console.error('Failed to save certificate:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่'
);
}
Fix 4: Add Global Error Handler for Unhandled Exceptions
Create/Update src/middlewares/error-handler.ts:
import { Request, Response, NextFunction } from 'express';
import HttpError from '../interfaces/http-error';
export function globalErrorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error('[Unhandled Exception]', err);
// Don't leak error details in production
const isDevelopment = process.env.NODE_ENV === 'development';
if (err instanceof HttpError) {
return res.status(err.status).json({
error: err.message,
...(isDevelopment && { stack: err.stack })
});
}
// Handle TypeError from result.errorMessage pattern
if (err instanceof TypeError && err.message.includes("errorMessage")) {
return res.status(500).json({
error: 'External service error',
...(isDevelopment && { details: err.message })
});
}
// Generic error response
res.status(500).json({
error: 'Internal server error',
...(isDevelopment && {
message: err.message,
stack: err.stack
})
});
}
// Handle unhandled promise rejections
export function setupUnhandledRejectionHandler() {
process.on('unhandledRejection', (reason, promise) => {
console.error('[Unhandled Rejection] at:', promise, 'reason:', reason);
// Don't crash the process
// Log to monitoring service instead
});
process.on('uncaughtException', (error) => {
console.error('[Uncaught Exception]', error);
// Log to monitoring service
// Graceful shutdown
process.exit(1);
});
}
Summary Statistics
| Issue Type | Count | Severity |
|---|---|---|
| Unhandled External API Call | 2 | CRITICAL |
| Incorrect Error Handling (TypeError Risk) | 8 | CRITICAL |
| Missing Transaction Support | 6 | HIGH |
| Silent Error Swallowing | 2 | MEDIUM |
| Memory/Performance Risk | Multiple | MEDIUM |
Files Requiring Immediate Attention
- ✅
src/controllers/ProfileController.ts- CRITICAL (Line 484, 5473, 5792) - ✅
src/controllers/ProfileChangeNameController.ts- CRITICAL (Line 118, 189) - ✅
src/controllers/ProfileChangeNameEmployeeController.ts- CRITICAL (Line 124, 189) - ✅
src/controllers/ProfileChangeNameEmployeeTempController.ts- CRITICAL (Line 116) - ✅
src/keycloak/index.ts- CRITICAL (Need to fix return type consistency)
Priority Recommendations
P0 (Immediate Action Required)
- Fix the
result.errorMessageTypeError pattern across all controllers - Add proper error handling for external API calls in ProfileController
- Implement global error handler for unhandled exceptions
P1 (This Sprint)
- Add transaction support for multi-table operations
- Implement retry logic for external API calls
- Add proper logging and monitoring
P2 (Next Sprint)
- Review memory usage with structuredClone()
- Add circuit breaker pattern for external services
- Implement comprehensive error tracking
Testing Recommendations
- Unit Tests: Test error scenarios for Keycloak integration
- Integration Tests: Test external API failure scenarios
- Load Tests: Test memory usage with large profile data
- Chaos Testing: Test behavior when external services are down
Report Generated: 2026-05-08 Batch: 08 (Controllers 71-80) Total Files Analyzed: 10 Critical Issues Found: 8