# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop ## Batch 10: Controllers 91-100 **วันที่ตรวจสอบ:** 2026-05-08 **จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers --- ## Controllers ที่ตรวจสอบในชุดนี้ 1. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts) 2. [SocketController.ts](src/controllers/SocketController.ts) 3. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts) 4. [ApiManageController.ts](src/controllers/ApiManageController.ts) 5. [ImportDataController.ts](src/controllers/ImportDataController.ts) 6. [ExRetirementController.ts](src/controllers/ExRetirementController.ts) 7. [IssuesController.ts](src/controllers/IssuesController.ts) 8. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts) 9. [MyController.ts](src/controllers/MyController.ts) 10. [MainController.ts](src/controllers/MainController.ts) --- ## รายการปัญหาที่พบ ### 1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop **File & Location:** [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts:159-182) - `syncByProfileIds()` method **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** ```typescript 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:** ```typescript @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](src/controllers/ImportDataController.ts:219-364) - `UploadFileSqlOfficer()` method **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** ```typescript @Post("uploadProfile-Officer") async UploadFileSqlOfficer(@Request() request: { user: Record }) { const OFFICER = await this.OFFICERRepo.find(); let rowCount = 0; // ... for await (const item of OFFICER) { rowCount++; // ... การประมวลผลข้อมูล ... await this.profileRepo.save(profile); } return new HttpSuccess(); } ``` **ปัญหาที่พบ:** 1. **ไม่มี try-catch รอบ loop** - หากเกิด error ระหว่างการประมวลผล เช่น: - Database connection lost - Invalid data format - Constraint violation - Memory overflow จะทำให้เกิด Unhandled Exception และ **Process Crash** 2. **ไม่มี Error Recovery** - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup 3. **Loading all data at once** - `await this.OFFICERRepo.find()` โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory 4. **No transaction management** - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้ **Recommended Fix:** ```typescript @Post("uploadProfile-Officer") async UploadFileSqlOfficer(@Request() request: { user: Record }) { 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](src/controllers/ImportDataController.ts:369-496) - `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](src/controllers/ImportDataController.ts:501-633) - `UploadFileSQLTemp()` method **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** ```typescript 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; } ``` **ปัญหาเพิ่มเติม:** 1. **Hardcoded citizenId check** - มีการ hardcode เงื่อนไข `item.CIT.toString() == "1101801164891"` ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ 2. **การ skip ที่ไม่ชัดเจน** - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่ 3. **ไม่มี error handling** เหมือนปัญหาก่อนหน้า **Recommended Fix:** ```typescript @Post("uploadProfile-EmployeeTemp") async UploadFileSQLTemp(@Request() request: { user: Record }) { 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](src/controllers/ExRetirementController.ts:148-173) - `getToken()` function **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript async function getToken(ClientID: string, ClientSecret: string): Promise { // ... 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 }); } } ``` **ปัญหา:** 1. **Generic error handling** - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป 2. **ไม่มี retry logic** - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ 3. **No timeout** - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด **Recommended Fix:** ```typescript async function getToken(ClientID: string, ClientSecret: string): Promise { 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](src/controllers/ApiWebServiceController.ts:67-78) - `listAttribute()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript if (system == "organization") { tbMain = "OrgRoot"; const revision = await this.orgRevisionRepository.findOne({ select: ["id"], where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; } ``` **ปัญหา:** 1. **revision อาจเป็น null** - หากไม่พบ revision ที่ตรงตามเงื่อนไข `revision?.id` จะเป็น `undefined` 2. **SQL Injection vulnerability** - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error 3. **ไม่มี error handling** - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception **Recommended Fix:** ```typescript @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 { 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 = {}; 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](src/controllers/ApiManageController.ts:464-518) - `createApi()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript @Post("") async createApi( @Request() req: RequestWithUser, @Body() apiData: CreateApi, ): Promise { 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(); } } ``` **ปัญหา:** 1. **validateSuperAdminRole อยู่นอก try-catch** - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak 2. **ไม่ validate req.user** ก่อนเรียก `validateSuperAdminRole` - หาก `req.user` เป็น null หรือ undefined จะเกิด error **Recommended Fix:** ```typescript @Post("") async createApi( @Request() req: RequestWithUser, @Body() apiData: CreateApi, ): Promise { 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](src/controllers/DevelopmentRequestController.ts:349-365) - `newDevelopmentRequest()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript 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 }); }), ); } ``` **ปัญหา:** 1. **Unhandled Promise rejection** - หาก `save` หรือ `setLogDataDiff` ล้ม จะเกิด unhandled rejection 2. **ไม่มี error handling รายตัว** - หาก project หนึ่งล้ม ทั้ง batch จะล้ม 3. **No cleanup on partial failure** - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database **Recommended Fix:** ```typescript 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](src/controllers/SocketController.ts:6-24) - `notify()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript @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 || [], }, ); } ``` **ปัญหา:** 1. **ไม่มี try-catch** - หาก `sendWebSocket` throw error จะเป็น unhandled exception 2. **ไม่ return ค่า** - ไม่มีการ return HttpSuccess หรือ error response 3. **No validation** - ไม่ validate payload ก่อนใช้งาน **Recommended Fix:** ```typescript @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](src/controllers/IssuesController.ts:31-39) - `getIssues()` method **Problem Type:** 2. Missing Error Handle **Root Cause:** ```typescript @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:** ```typescript @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) 1. **ImportDataController.ts** - ทั้ง 3 methods (`UploadFileSqlOfficer`, `UploadFileSQL`, `UploadFileSQLTemp`) - เพิ่ม transaction management - เพิ่ม try-catch ใน loops - เพิ่ม pagination แทนการโหลดทั้งหมด ### แก้ไขเร็วๆ นี้ (P1 - High) 2. **KeycloakSyncController.ts** - เพิ่ม timeout protection 3. **ExRetirementController.ts** - ปรับปรุง error handling และ retry logic 4. **ApiWebServiceController.ts** - แก้ null reference issue ### แก้ไขในภายหลัง (P2 - Medium) 5. **ApiManageController.ts** - ปรับปรุง transaction error handling 6. **DevelopmentRequestController.ts** - เพิ่ม error handling สำหรับ parallel operations ### แก้ไขเมื่อว่าง (P3 - Low) 7. **SocketController.ts** - เพิ่ม validation และ error handling 8. **IssuesController.ts** - เพิ่ม try-catch --- ## ข้อเสนอแนะเพิ่มเติม 1. **ใช้ Global Error Handler** - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling 2. **เพิ่ม Health Check** - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API) 3. **Circuit Breaker Pattern** - สำหรับการเรียก external API เพื่อป้องกัน cascade failures 4. **Graceful Shutdown** - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown 5. **Logging Strategy** - เพิ่ม structured logging สำหรับ monitoring และ debugging --- ## ไฟล์รายงานที่เกี่ยวข้อง - [Batch 1-10 Analysis](../reports/) - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า - [Security Audit Report](../reports/security-audit.md) - รายงานการตรวจสอบด้านความปลอดภัย