Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4585801f32 | |||
| 3dc75f5cda | |||
| 7591e0ed90 | |||
| d2c2a4c4dd |
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
||||
# 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
|
||||
|
||||
- Introduces fetchWithRetry(url, options, timeoutMs, maxRetries) to wrap fetch with AbortSignal timeout, exponential backoff retries, and retry behavior only for network errors and 5xx responses
|
||||
- Replaces direct fetch calls for registry /v2 checks, token requests, and blob uploads with fetchWithRetry (30s for auth/token checks, 300s for blob operations)
|
||||
- Clears token cache entry when a 401 response is received so the next attempt re-authenticates
|
||||
- Adds logging on retry attempts and backoff delays to improve robustness and observability
|
||||
|
||||
## 2026-02-07 - 1.17.0 - feat(tsdocker)
|
||||
add Dockerfile filtering, optional skip-build flow, and fallback Docker config credential loading
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tsdocker",
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.2",
|
||||
"private": false,
|
||||
"description": "develop npm modules cross platform with docker",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsdocker',
|
||||
version: '1.17.0',
|
||||
version: '1.17.2',
|
||||
description: 'develop npm modules cross platform with docker'
|
||||
}
|
||||
|
||||
@@ -20,6 +20,53 @@ interface ITokenCache {
|
||||
export class RegistryCopy {
|
||||
private tokenCache: ITokenCache = {};
|
||||
|
||||
/**
|
||||
* Wraps fetch() with timeout (via AbortSignal) and retry with exponential backoff.
|
||||
* Retries on network errors and 5xx; does NOT retry on 4xx client errors.
|
||||
* On 401, clears the token cache entry so the next attempt re-authenticates.
|
||||
*/
|
||||
private async fetchWithRetry(
|
||||
url: string,
|
||||
options: RequestInit & { duplex?: string },
|
||||
timeoutMs: number = 300_000,
|
||||
maxRetries: number = 3,
|
||||
): Promise<Response> {
|
||||
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) {
|
||||
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', `${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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Docker credentials from ~/.docker/config.json for a given registry.
|
||||
* Supports base64-encoded "auth" field in the config.
|
||||
@@ -109,7 +156,7 @@ export class RegistryCopy {
|
||||
}
|
||||
|
||||
try {
|
||||
const checkResp = await fetch(`${apiBase}/v2/`, { method: 'GET' });
|
||||
const checkResp = await this.fetchWithRetry(`${apiBase}/v2/`, { method: 'GET' }, 30_000);
|
||||
if (checkResp.ok) return null; // No auth needed
|
||||
|
||||
const wwwAuth = checkResp.headers.get('www-authenticate') || '';
|
||||
@@ -131,7 +178,7 @@ export class RegistryCopy {
|
||||
headers['Authorization'] = 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
||||
}
|
||||
|
||||
const tokenResp = await fetch(tokenUrl.toString(), { headers });
|
||||
const tokenResp = await this.fetchWithRetry(tokenUrl.toString(), { headers }, 30_000);
|
||||
if (!tokenResp.ok) {
|
||||
const body = await tokenResp.text();
|
||||
throw new Error(`Token request failed (${tokenResp.status}): ${body}`);
|
||||
@@ -189,7 +236,16 @@ export class RegistryCopy {
|
||||
fetchOptions.duplex = 'half'; // Required for streaming body in Node
|
||||
}
|
||||
|
||||
return fetch(url, fetchOptions);
|
||||
const resp = await this.fetchWithRetry(url, fetchOptions, 300_000);
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,11 +376,11 @@ export class RegistryCopy {
|
||||
putHeaders['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const putResp = await fetch(putUrl, {
|
||||
const putResp = await this.fetchWithRetry(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: putHeaders,
|
||||
body: blobData,
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
if (!putResp.ok) {
|
||||
const body = await putResp.text();
|
||||
|
||||
Reference in New Issue
Block a user