diff --git a/changelog.md b/changelog.md index 2cd1188..30e608b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-07 - 1.17.2 - fix(registry) +improve HTTP fetch retry logging, backoff calculation, and token-cache warning + +- Include HTTP method in logs and normalize method to uppercase for consistency +- Log retry attempts with method, URL and calculated exponential backoff delay +- Compute and reuse exponential backoff delay variable instead of inline calculation +- Log error when a 5xx response persists after all retry attempts and when fetch ultimately fails +- Add a warning log when clearing cached token after a 401 response + ## 2026-02-07 - 1.17.1 - fix(registrycopy) add fetchWithRetry wrapper to apply timeouts, retries with exponential backoff, and token cache handling; use it for registry HTTP requests diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 7edf473..060950d 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsdocker', - version: '1.17.1', + version: '1.17.2', description: 'develop npm modules cross platform with docker' } diff --git a/ts/classes.registrycopy.ts b/ts/classes.registrycopy.ts index b37f08a..b48e6c3 100644 --- a/ts/classes.registrycopy.ts +++ b/ts/classes.registrycopy.ts @@ -31,26 +31,36 @@ export class RegistryCopy { timeoutMs: number = 300_000, maxRetries: number = 3, ): Promise { + const method = (options.method || 'GET').toUpperCase(); let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { + if (attempt > 1) { + logger.log('info', `Retry ${attempt}/${maxRetries} for ${method} ${url}`); + } const resp = await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs), }); // Retry on 5xx server errors (but not 4xx) if (resp.status >= 500 && attempt < maxRetries) { - logger.log('warn', `Request to ${url} returned ${resp.status}, retrying (${attempt}/${maxRetries})...`); - await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1))); + const delay = 1000 * Math.pow(2, attempt - 1); + logger.log('warn', `${method} ${url} returned ${resp.status}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`); + await new Promise(r => setTimeout(r, delay)); continue; } + if (resp.status >= 500) { + logger.log('error', `${method} ${url} returned ${resp.status} after ${maxRetries} attempts, giving up`); + } return resp; } catch (err) { lastError = err as Error; if (attempt < maxRetries) { const delay = 1000 * Math.pow(2, attempt - 1); - logger.log('warn', `fetch failed (attempt ${attempt}/${maxRetries}): ${lastError.message}, retrying in ${delay}ms...`); + logger.log('warn', `${method} ${url} failed (attempt ${attempt}/${maxRetries}): ${lastError.message}, retrying in ${delay}ms...`); await new Promise(r => setTimeout(r, delay)); + } else { + logger.log('error', `${method} ${url} failed after ${maxRetries} attempts: ${lastError.message}`); } } } @@ -231,6 +241,7 @@ export class RegistryCopy { // Token expired — clear cache so next call re-authenticates if (resp.status === 401 && token) { const cacheKey = `${registry}/${`repository:${repo}:${actions}`}`; + logger.log('warn', `Got 401 for ${registry}${path} — clearing cached token for ${cacheKey}`); delete this.tokenCache[cacheKey]; }