diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index 6a19ddfc..c9c45dae 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -43,6 +43,35 @@ class TokenCache { } } +// network/DNS error codes ที่ถือว่าเป็น transient (ควร retry) +const TRANSIENT_NETWORK_CODES = [ + "EAI_AGAIN", // DNS lookup ล้มเหลวชั่วคราว + "ENOTFOUND", + "ECONNRESET", + "ETIMEDOUT", + "ECONNREFUSED", + "EHOSTUNREACH", + "ENETUNREACH", +]; + +// ตรวจว่า error ควร retry: ครอบ network/DNS (ไม่มี response หรือ code ตรง TRANSIENT) และ HTTP 5xx +function isTransientError(error: any): boolean { + if (!error) return false; + if (!error.response || TRANSIENT_NETWORK_CODES.includes(error.code)) { + return true; + } + return error.response?.status >= 500; +} + +// หน่วงเวลา (backoff) ระหว่าง retry — ใช้รูปแบบ setTimeout เดียวกับ keycloak/index.ts +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// config retry สำหรับเรียก Exprofile +const EXPROFILE_MAX_RETRIES = 3; // จากเดิม 2 +const EXPROFILE_BACKOFF_BASE_MS = 1000; // backoff = base * 2^retryCount → 1s, 2s + @Route("api/v1/org/ex/retirement") @Tags("ExRetirement") @Security("bearerAuth") @@ -60,7 +89,7 @@ export class ExRetirementController extends Controller { }, ) { let retryCount = 0; - const maxRetries = 2; + const maxRetries = EXPROFILE_MAX_RETRIES; while (retryCount < maxRetries) { try { @@ -92,8 +121,9 @@ export class ExRetirementController extends Controller { // return res.data; return new HttpSuccess(res.data.data); } catch (error: any) { - if (error.response?.status === 500 && retryCount < maxRetries - 1) { + if (isTransientError(error) && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); + await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; } @@ -107,7 +137,7 @@ export class ExRetirementController extends Controller { @Get("/document/{documentId}") async getDocument(@Path("documentId") officerDocumentID: string, @Request() req: any) { let retryCount = 0; - const maxRetries = 2; + const maxRetries = EXPROFILE_MAX_RETRIES; while (retryCount < maxRetries) { try { const token = await getToken(clientId, clientSecret); @@ -136,8 +166,9 @@ export class ExRetirementController extends Controller { return; } } catch (error: any) { - if (error.response?.status === 500 && retryCount < maxRetries - 1) { + if (isTransientError(error) && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); + await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; } @@ -171,8 +202,13 @@ async function getToken(ClientID: string, ClientSecret: string): Promise TokenCache.set(cacheKey, token); return token; } catch (error) { - console.error('getToken error:', error); - return Promise.reject({ message: "Error occurred", error }); + // log แบบกระชับ (ลด log noise จากการ dump object ใหญ่ตามที่เห็นใน log จริง) + console.error( + "getToken error:", + error instanceof Error ? `${error.name}: ${error.message}` : error, + ); + // โยน AxiosError ตัวจริงออกไปให้ caller อ่าน code / response / status เพื่อตัดสินใจ retry + throw error; } } @@ -198,7 +234,7 @@ export async function PostRetireToExprofile( } let retryCount = 0; - const maxRetries = 2; + const maxRetries = EXPROFILE_MAX_RETRIES; while (retryCount < maxRetries) { try { @@ -235,8 +271,9 @@ export async function PostRetireToExprofile( return res.data; } catch (error: any) { - if (error.response?.status === 500 && retryCount < maxRetries - 1) { + if (isTransientError(error) && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); + await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount); retryCount++; continue; }