37 KiB
รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
Batch 10: Controllers 91-100
วันที่ตรวจสอบ: 2026-05-08
จำนวน Controllers ที่ตรวจสอบ: 10 Controllers
Controllers ที่ตรวจสอบในชุดนี้
- KeycloakSyncController.ts
- SocketController.ts
- ApiWebServiceController.ts
- ApiManageController.ts
- ImportDataController.ts
- ExRetirementController.ts
- IssuesController.ts
- DevelopmentRequestController.ts
- MyController.ts
- MainController.ts
รายการปัญหาที่พบ
1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop
File & Location: KeycloakSyncController.ts - syncByProfileIds() method
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
for (const profileId of profileIds) {
try {
const success = await this.keycloakAttributeService.syncOnOrganizationChange(
profileId,
profileType,
);
// ...
} catch (error: any) {
result.failed++;
result.details.push({ profileId, status: "failed", error: error.message });
}
}
แม้ว่าจะมี try-catch ภายใน loop แต่การ catch error แล้วเพียงแค่บันทึกผลลัพธ์ อาจไม่เพียงพอสำหรับบางกรณี:
- หาก
syncOnOrganizationChangeมี Promise rejection ที่ไม่ถูก handle อย่างถูกต้องภายใน service - หากเกิด error ระหว่างการทำงานของ loop ที่ไม่ใช่จาก
syncOnOrganizationChangeเช่น จากresult.details.push() - Error ที่เกิดขึ้นอาจเป็น unhandled rejection หาก service ไม่ return Promise อย่างถูกต้อง
Recommended Fix:
@Post("sync-profiles-batch")
async syncByProfileIds(
@Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
) {
const { profileIds, profileType } = request;
if (!profileIds || profileIds.length === 0) {
throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
}
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
);
}
const result = {
total: profileIds.length,
success: 0,
failed: 0,
details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
};
// เพิ่ม timeout protection และ error handling ที่ดีขึ้น
const SYNC_TIMEOUT = 30000; // 30 วินาทีต่อ profile
for (const profileId of profileIds) {
try {
// เพิ่ม Promise.race เพื่อป้องกันการ hang
const syncPromise = this.keycloakAttributeService.syncOnOrganizationChange(
profileId,
profileType,
);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT)
);
const success = await Promise.race([syncPromise, timeoutPromise]) as boolean;
if (success) {
result.success++;
result.details.push({ profileId, status: "success" });
} else {
result.failed++;
result.details.push({
profileId,
status: "failed",
error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID",
});
}
} catch (error: any) {
result.failed++;
// เพิ่ม validation ก่อน push เพื่อป้องกัน crash จาก invalid data
const errorMessage = error?.message || String(error);
result.details.push({
profileId,
status: "failed",
error: errorMessage.substring(0, 500) // จำกัดความยาว
});
// Log error สำหรับ monitoring
console.error(`[KeycloakSync] Failed to sync profile ${profileId}:`, error);
}
}
return new HttpSuccess({
message: "Batch sync เสร็จสิ้น",
...result,
});
}
2. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Large Loop
File & Location: ImportDataController.ts - UploadFileSqlOfficer() method
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
const OFFICER = await this.OFFICERRepo.find();
let rowCount = 0;
// ...
for await (const item of OFFICER) {
rowCount++;
// ... การประมวลผลข้อมูล ...
await this.profileRepo.save(profile);
}
return new HttpSuccess();
}
ปัญหาที่พบ:
-
ไม่มี try-catch รอบ loop - หากเกิด error ระหว่างการประมวลผล เช่น:
- Database connection lost
- Invalid data format
- Constraint violation
- Memory overflow
จะทำให้เกิด Unhandled Exception และ Process Crash
-
ไม่มี Error Recovery - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup
-
Loading all data at once -
await this.OFFICERRepo.find()โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory -
No transaction management - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้
Recommended Fix:
@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
let rowCount = 0;
let successCount = 0;
let failedCount = 0;
const errors: Array<{row: number, citizenId: string, error: string}> = [];
try {
// ใช้ pagination แทนการโหลดทั้งหมด
const BATCH_SIZE = 500;
let offset = 0;
let hasMore = true;
while (hasMore) {
const OFFICER = await queryRunner.manager.find(OFFICER, {
take: BATCH_SIZE,
skip: offset,
order: { id: 'ASC' }
});
if (OFFICER.length === 0) {
hasMore = false;
break;
}
for (const item of OFFICER) {
rowCount++;
try {
let type_: any = null;
let level_: any = null;
const profile = new Profile();
const existingProfile = await queryRunner.manager.findOne(Profile, {
where: { citizenId: item.CIT.toString() },
});
if (existingProfile) {
// ข้ามกรณีมีข้อมูลอยู่แล้ว
continue;
}
// ... การประมวลผลข้อมูลเดิม ...
// ใช้ queryRunner.manager.save แทน this.profileRepo.save
await queryRunner.manager.save(profile);
successCount++;
} catch (itemError: any) {
failedCount++;
errors.push({
row: rowCount,
citizenId: item.CIT?.toString() || 'unknown',
error: itemError?.message || String(itemError)
});
// Log แต่ไม่หยุดการทำงาน
console.error(`[UploadOfficer] Error at row ${rowCount}:`, itemError);
}
}
offset += BATCH_SIZE;
// Commit ทุกๆ batch เพื่อป้องกัน transaction ใหญ่เกินไป
await queryRunner.commitTransaction();
await queryRunner.startTransaction();
}
// Commit transaction สุดท้าย
await queryRunner.commitTransaction();
return new HttpSuccess({
message: "อัปโหลดข้อมูลเสร็จสิ้น",
total: rowCount,
success: successCount,
failed: failedCount,
errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก
});
} catch (error: any) {
await queryRunner.rollbackTransaction();
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
`ไม่สามารถอัปโหลดข้อมูลได้: ${error?.message || 'Unknown error'}`
);
} finally {
await queryRunner.release();
}
}
3. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Employee Upload
File & Location: ImportDataController.ts - UploadFileSQL() method
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause: เหมือนกับปัญหาข้างต้น แต่สำหรับการอัปโหลดข้อมูลลูกจ้างประจำ มีความเสี่ยงเช่นเดียวกัน:
- ไม่มี try-catch ใน loop
- ไม่มี transaction management
- ไม่มี error recovery
Recommended Fix: ใช้ pattern เดียวกับข้อ 2 โดยใช้ QueryRunner สำหรับ transaction management
4. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Temp Employee Upload
File & Location: ImportDataController.ts - UploadFileSQLTemp() method
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
if (item.CIT.toString() == "1101801164891") {
continue;
}
const existingProfile = await this.profileEmpRepo.findOne({
where: { employeeClass: "TEMP", citizenId: item.CIT.toString() },
});
if (existingProfile) {
profile.id = existingProfile.id;
} else {
continue;
}
ปัญหาเพิ่มเติม:
- Hardcoded citizenId check - มีการ hardcode เงื่อนไข
item.CIT.toString() == "1101801164891"ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ - การ skip ที่ไม่ชัดเจน - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่
- ไม่มี error handling เหมือนปัญหาก่อนหน้า
Recommended Fix:
@Post("uploadProfile-EmployeeTemp")
async UploadFileSQLTemp(@Request() request: { user: Record<string, any> }) {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
let rowCount = 0;
let successCount = 0;
let failedCount = 0;
const errors: Array<{row: number, citizenId: string, error: string}> = [];
try {
const BATCH_SIZE = 500;
let offset = 0;
let hasMore = true;
while (hasMore) {
const EMPLOYEE = await queryRunner.manager.find(EMPLOYEETEMP, {
take: BATCH_SIZE,
skip: offset,
order: { id: 'ASC' }
});
if (EMPLOYEE.length === 0) {
hasMore = false;
break;
}
for (const item of EMPLOYEE) {
rowCount++;
try {
// เอา hardcode check ออก หรือเปลี่ยนเป็น configurable
// if (item.CIT.toString() === "1101801164891") {
// continue;
// }
const existingProfile = await queryRunner.manager.findOne(ProfileEmployee, {
where: {
employeeClass: "TEMP",
citizenId: item.CIT.toString()
},
});
let profile: ProfileEmployee;
if (existingProfile) {
profile = existingProfile;
} else {
// สร้าง profile ใหม่ถ้าไม่พบ
profile = new ProfileEmployee();
profile.employeeClass = "TEMP";
}
// ... การประมวลผลข้อมูลเดิม ...
await queryRunner.manager.save(profile);
successCount++;
} catch (itemError: any) {
failedCount++;
errors.push({
row: rowCount,
citizenId: item.CIT?.toString() || 'unknown',
error: itemError?.message || String(itemError)
});
console.error(`[UploadEmployeeTemp] Error at row ${rowCount}:`, itemError);
}
}
offset += BATCH_SIZE;
await queryRunner.commitTransaction();
await queryRunner.startTransaction();
}
await queryRunner.commitTransaction();
return new HttpSuccess({
message: "อัปโหลดข้อมูลลูกจ้างชั่วคราวเสร็จสิ้น",
total: rowCount,
success: successCount,
failed: failedCount,
errors: errors.slice(0, 100)
});
} catch (error: any) {
await queryRunner.rollbackTransaction();
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
`ไม่สามารถอัปโหลดข้อมูลลูกจ้างชั่วคราวได้: ${error?.message || 'Unknown error'}`
);
} finally {
await queryRunner.release();
}
}
5. 🟡 HIGH - ExRetirementController.ts - Unhandled External API Error
File & Location: ExRetirementController.ts - getToken() function
Problem Type: 2. Missing Error Handle
Root Cause:
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
// ...
try {
const formData = new FormData();
formData.append("ClientID", ClientID);
formData.append("ClientSecret", ClientSecret);
const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
headers: {
"Content-Type": "application/json",
},
});
const token = res.data.token;
TokenCache.set(cacheKey, token);
return token;
} catch (error) {
return Promise.reject({ message: "Error occurred", error });
}
}
ปัญหา:
- Generic error handling - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป
- ไม่มี retry logic - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ
- No timeout - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด
Recommended Fix:
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
const cacheKey = `${ClientID}:${ClientSecret}`;
const cachedToken = TokenCache.get(cacheKey);
if (cachedToken) {
return cachedToken;
}
const MAX_RETRIES = 3;
const TIMEOUT = 10000; // 10 วินาที
let lastError: any;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const formData = new FormData();
formData.append("ClientID", ClientID);
formData.append("ClientSecret", ClientSecret);
const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
headers: {
"Content-Type": "application/json",
},
timeout: TIMEOUT,
});
const token = res.data.token;
if (!token) {
throw new Error('Token not found in response');
}
TokenCache.set(cacheKey, token);
return token;
} catch (error: any) {
lastError = error;
// ไม่ retry หากเป็น client error (4xx)
if (error.response?.status >= 400 && error.response?.status < 500) {
break;
}
// Retry หากเป็น server error หรือ network error
if (attempt < MAX_RETRIES) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Log error สำหรับ monitoring
console.error(`[ExRetirement] Failed to get token after ${MAX_RETRIES} attempts:`, lastError);
throw new Error(`ไม่สามารถขอ Token ได้: ${lastError?.message || 'Unknown error'}`);
}
6. 🟡 HIGH - ApiWebServiceController.ts - Potential Null Reference
File & Location: ApiWebServiceController.ts - listAttribute() method
Problem Type: 2. Missing Error Handle
Root Cause:
if (system == "organization") {
tbMain = "OrgRoot";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
condition = `OrgRoot.orgRevisionId = "${revision?.id}"`;
}
ปัญหา:
- revision อาจเป็น null - หากไม่พบ revision ที่ตรงตามเงื่อนไข
revision?.idจะเป็นundefined - SQL Injection vulnerability - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error
- ไม่มี error handling - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception
Recommended Fix:
@Get("/:system/:code")
async listAttribute(
@Request() request: RequestWithUserWebService,
@Path("system")
system: SystemCode,
@Path("code") code: string,
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 100,
): Promise<HttpSuccess | HttpError> {
try {
const apiName = await this.apiNameRepository.findOne({
where: { code },
select: ["id", "code", "methodApi", "system", "isActive"],
relations: ["apiAttributes"],
order: {
apiAttributes: {
ordering: "ASC",
},
},
});
if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ");
}
await isPermissionRequest(request, apiName.id);
const offset = (page - 1) * pageSize;
const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`);
let tbMain: string = "";
let condition: string = "1=1";
let revisionId: string | null = null;
if (system == "registry") {
tbMain = "Profile";
} else if (system == "registry_emp") {
tbMain = "ProfileEmployee";
condition = `ProfileEmployee.employeeClass = "PERM"`;
} else if (system == "registry_temp") {
tbMain = "ProfileEmployee";
condition = `ProfileEmployee.employeeClass = "TEMP"`;
} else if (system == "organization") {
tbMain = "OrgRoot";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
if (!revision) {
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
}
revisionId = revision.id;
condition = `OrgRoot.orgRevisionId = :revisionId`;
} else if (system == "position") {
tbMain = "PosMaster";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
if (!revision) {
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
}
revisionId = revision.id;
condition = `PosMaster.orgRevisionId = :revisionId`;
}
const repo = AppDataSource.getRepository(tbMain);
const metadata = repo.metadata;
const relationMap: Record<string, string> = {};
metadata.relations.forEach((rel) => {
relationMap[rel.inverseEntityMetadata.name] = rel.propertyName;
});
let propertyOtherKey: any[] = [];
propertyOtherKey = [
...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)),
];
const queryBuilder = repo.createQueryBuilder(tbMain);
if (propertyOtherKey.length > 0) {
propertyOtherKey.forEach((tb) => {
const relationName = relationMap[tb];
if (relationName) {
queryBuilder.leftJoin(
`${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`,
tb,
);
}
});
}
let pk: string = "";
const primaryColumns = metadata.primaryColumns;
primaryColumns.forEach((col) => {
pk = col.propertyName;
if (!propertyKey.includes(`${tbMain}.${pk}`)) {
propertyKey.push(`${tbMain}.${pk}`);
}
});
// ใช้ parameterized query แทน string interpolation
const queryParams: any = {};
if (revisionId) {
queryParams.revisionId = revisionId;
}
const [items, total] = await queryBuilder
.select(propertyKey)
.where(condition, queryParams)
.orderBy(propertyKey[0], "ASC")
.skip(offset)
.take(pageSize)
.getManyAndCount();
const data = items.map((item) => {
const { [pk]: removedPk, ...x } = item;
return x;
});
// save api history after query success
const history = {
headerApi: JSON.stringify({
host: request.headers.host,
"x-api-key": request.headers["x-api-key"],
connection: request.headers.connection,
accept: request.headers.accept,
}),
tokenApi: Array.isArray(request.headers["x-api-key"])
? request.headers["x-api-key"][0] || ""
: request.headers["x-api-key"] || "",
requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`,
responseApi: "OK",
ipApi: request.ip,
codeApi: code,
apiKeyId: request.user.id,
apiNameId: apiName.id,
createdFullName: request.user.name,
lastUpdateFullName: request.user.name,
};
try {
await this.apiHistoryRepository.save(history);
} catch (historyError) {
// Log แต่ไม่ให้กระทบต่อ response
console.error('[ApiWebService] Failed to save history:', historyError);
}
return new HttpSuccess({ data: data, total });
} catch (error: any) {
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
`เกิดข้อผิดพลาด: ${error?.message || 'Unknown error'}`
);
}
}
7. 🟡 MEDIUM - ApiManageController.ts - Missing Transaction Error Handling
File & Location: ApiManageController.ts - createApi() method
Problem Type: 2. Missing Error Handle
Root Cause:
@Post("")
async createApi(
@Request() req: RequestWithUser,
@Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
this.validateSuperAdminRole(req.user);
// ...
await queryRunner.commitTransaction();
return new HttpSuccess(apiName.id);
} catch (error) {
await queryRunner.rollbackTransaction();
throw new HttpError(...);
} finally {
await queryRunner.release();
}
}
ปัญหา:
- validateSuperAdminRole อยู่นอก try-catch - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak
- ไม่ validate req.user ก่อนเรียก
validateSuperAdminRole- หากreq.userเป็น null หรือ undefined จะเกิด error
Recommended Fix:
@Post("")
async createApi(
@Request() req: RequestWithUser,
@Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Validate request ก่อน
if (!req.user) {
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้");
}
this.validateSuperAdminRole(req.user);
const code = this.generateApiCode();
const postData = {
name: apiData.name,
code,
pathApi: this.createApiPath(apiData.system as SystemCode, code),
methodApi: apiData.methodApi || "GET",
system: apiData.system || "registry",
isActive: apiData.isActive || false,
createdUserId: req.user?.sub,
createdFullName: req.user?.name || "",
};
const apiName = await queryRunner.manager.getRepository(ApiName).save(postData);
if (apiData.apiAttributes?.length) {
let orderingCounter = 0;
const attributesToSave = apiData.apiAttributes.flatMap((attr) =>
attr.propertyKey.map((propertyKey) => ({
apiNameId: apiName.id,
tbName: attr.tbName,
propertyKey,
ordering: orderingCounter++,
createdUserId: req.user?.sub,
createdFullName: req.user?.name || "",
})),
);
await queryRunner.manager.getRepository(ApiAttribute).save(attributesToSave);
}
await queryRunner.commitTransaction();
return new HttpSuccess(apiName.id);
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
error instanceof Error ? error.message : "เกิดข้อผิดพลาด ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่ในภายหลัง",
);
} finally {
// Ensure release is called even if rollback fails
try {
await queryRunner.release();
} catch (releaseError) {
console.error('[ApiManage] Failed to release queryRunner:', releaseError);
}
}
}
8. 🟡 MEDIUM - DevelopmentRequestController.ts - Unhandled Promise in Parallel Operations
File & Location: DevelopmentRequestController.ts - newDevelopmentRequest() method
Problem Type: 2. Missing Error Handle
Root Cause:
if (body.developmentProjects != null) {
await Promise.all(
body.developmentProjects.map(async (x) => {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdateUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before, after: developmentProject });
}),
);
}
ปัญหา:
- Unhandled Promise rejection - หาก
saveหรือsetLogDataDiffล้ม จะเกิด unhandled rejection - ไม่มี error handling รายตัว - หาก project หนึ่งล้ม ทั้ง batch จะล้ม
- No cleanup on partial failure - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database
Recommended Fix:
if (body.developmentProjects != null) {
const savedProjects: DevelopmentProject[] = [];
try {
for (const projectName of body.developmentProjects) {
try {
let developmentProject = new DevelopmentProject();
developmentProject.name = projectName;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdateUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
const saved = await this.developmentProjectRepository.save(developmentProject, { data: req });
savedProjects.push(saved);
setLogDataDiff(req, { before: null, after: saved });
} catch (projectError: any) {
console.error(`[DevelopmentRequest] Failed to save project ${projectName}:`, projectError);
// Continue with next project instead of failing entire request
}
}
} catch (batchError: any) {
console.error('[DevelopmentRequest] Error in projects batch:', batchError);
// Clean up any successfully saved projects if needed
if (savedProjects.length > 0) {
try {
await this.developmentProjectRepository.delete({
developmentRequestId: data.id
});
} catch (cleanupError) {
console.error('[DevelopmentRequest] Failed to cleanup projects:', cleanupError);
}
}
throw batchError;
}
}
9. 🟢 LOW - SocketController.ts - No Error Handling
File & Location: SocketController.ts - notify() method
Problem Type: 2. Missing Error Handle
Root Cause:
@Post("notify")
async notify(
@Body()
payload: {
message: string;
userId?: string | string[];
roles?: string | string[];
error?: boolean;
},
) {
sendWebSocket(
"socket-notification",
{ success: !payload.error, message: payload.message },
{
roles: payload.roles || [],
userId: payload.userId || [],
},
);
}
ปัญหา:
- ไม่มี try-catch - หาก
sendWebSocketthrow error จะเป็น unhandled exception - ไม่ return ค่า - ไม่มีการ return HttpSuccess หรือ error response
- No validation - ไม่ validate payload ก่อนใช้งาน
Recommended Fix:
@Post("notify")
async notify(
@Body()
payload: {
message: string;
userId?: string | string[];
roles?: string | string[];
error?: boolean;
},
) {
try {
// Validate payload
if (!payload.message || typeof payload.message !== 'string') {
throw new HttpError(HttpStatus.BAD_REQUEST, "message ต้องเป็น string ที่ไม่ว่างเปล่า");
}
// Validate userId and roles
if (payload.userId && !Array.isArray(payload.userId) && typeof payload.userId !== 'string') {
throw new HttpError(HttpStatus.BAD_REQUEST, "userId ต้องเป็น string หรือ array of strings");
}
if (payload.roles && !Array.isArray(payload.roles) && typeof payload.roles !== 'string') {
throw new HttpError(HttpStatus.BAD_REQUEST, "roles ต้องเป็น string หรือ array of strings");
}
sendWebSocket(
"socket-notification",
{ success: !payload.error, message: payload.message },
{
roles: payload.roles || [],
userId: payload.userId || [],
},
);
return new HttpSuccess({
message: "ส่งการแจ้งเตือนสำเร็จ",
notification: {
type: "socket-notification",
success: !payload.error,
message: payload.message,
roles: payload.roles || [],
userId: payload.userId || [],
}
});
} catch (error: any) {
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
`ไม่สามารถส่งการแจ้งเตือนได้: ${error?.message || 'Unknown error'}`
);
}
}
10. 🟢 LOW - IssuesController.ts - Missing Try-Catch
File & Location: IssuesController.ts - getIssues() method
Problem Type: 2. Missing Error Handle
Root Cause:
@Get("lists")
async getIssues() {
const issues = await this.issuesRepository.find({
order: {
createdAt: "DESC",
},
});
return new HttpSuccess(issues);
}
ปัญหา:
- ไม่มี try-catch หาก database connection ล้มหรือ query มีปัญหา จะเกิด unhandled exception
Recommended Fix:
@Get("lists")
async getIssues() {
try {
const issues = await this.issuesRepository.find({
order: {
createdAt: "DESC",
},
});
return new HttpSuccess(issues);
} catch (error: any) {
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
`ไม่สามารถดึงรายการปัญหาได้: ${error?.message || 'Unknown error'}`
);
}
}
สรุปสถิติ
| ระดับความรุนแรง | จำนวน | รายการ |
|---|---|---|
| 🔴 CRITICAL | 4 | 1, 2, 3, 4 |
| 🟡 HIGH | 2 | 5, 6 |
| 🟡 MEDIUM | 2 | 7, 8 |
| 🟢 LOW | 2 | 9, 10 |
คำแนะนำการจัดลำดับการแก้ไข
แก้ไขทันที (P0 - Critical)
- ImportDataController.ts - ทั้ง 3 methods (
UploadFileSqlOfficer,UploadFileSQL,UploadFileSQLTemp)- เพิ่ม transaction management
- เพิ่ม try-catch ใน loops
- เพิ่ม pagination แทนการโหลดทั้งหมด
แก้ไขเร็วๆ นี้ (P1 - High)
- KeycloakSyncController.ts - เพิ่ม timeout protection
- ExRetirementController.ts - ปรับปรุง error handling และ retry logic
- ApiWebServiceController.ts - แก้ null reference issue
แก้ไขในภายหลัง (P2 - Medium)
- ApiManageController.ts - ปรับปรุง transaction error handling
- DevelopmentRequestController.ts - เพิ่ม error handling สำหรับ parallel operations
แก้ไขเมื่อว่าง (P3 - Low)
- SocketController.ts - เพิ่ม validation และ error handling
- IssuesController.ts - เพิ่ม try-catch
ข้อเสนอแนะเพิ่มเติม
- ใช้ Global Error Handler - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling
- เพิ่ม Health Check - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API)
- Circuit Breaker Pattern - สำหรับการเรียก external API เพื่อป้องกัน cascade failures
- Graceful Shutdown - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown
- Logging Strategy - เพิ่ม structured logging สำหรับ monitoring และ debugging
ไฟล์รายงานที่เกี่ยวข้อง
- Batch 1-10 Analysis - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า
- Security Audit Report - รายงานการตรวจสอบด้านความปลอดภัย