Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7591e0ed90 | |||
| d2c2a4c4dd |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## 2026-02-07 - 1.17.0 - feat(tsdocker)
|
||||||
add Dockerfile filtering, optional skip-build flow, and fallback Docker config credential loading
|
add Dockerfile filtering, optional skip-build flow, and fallback Docker config credential loading
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsdocker",
|
"name": "@git.zone/tsdocker",
|
||||||
"version": "1.17.0",
|
"version": "1.17.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "develop npm modules cross platform with docker",
|
"description": "develop npm modules cross platform with docker",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsdocker',
|
name: '@git.zone/tsdocker',
|
||||||
version: '1.17.0',
|
version: '1.17.1',
|
||||||
description: 'develop npm modules cross platform with docker'
|
description: 'develop npm modules cross platform with docker'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,43 @@ interface ITokenCache {
|
|||||||
export class RegistryCopy {
|
export class RegistryCopy {
|
||||||
private tokenCache: ITokenCache = {};
|
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> {
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
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)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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...`);
|
||||||
|
await new Promise(r => setTimeout(r, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads Docker credentials from ~/.docker/config.json for a given registry.
|
* Reads Docker credentials from ~/.docker/config.json for a given registry.
|
||||||
* Supports base64-encoded "auth" field in the config.
|
* Supports base64-encoded "auth" field in the config.
|
||||||
@@ -109,7 +146,7 @@ export class RegistryCopy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
if (checkResp.ok) return null; // No auth needed
|
||||||
|
|
||||||
const wwwAuth = checkResp.headers.get('www-authenticate') || '';
|
const wwwAuth = checkResp.headers.get('www-authenticate') || '';
|
||||||
@@ -131,7 +168,7 @@ export class RegistryCopy {
|
|||||||
headers['Authorization'] = 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
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) {
|
if (!tokenResp.ok) {
|
||||||
const body = await tokenResp.text();
|
const body = await tokenResp.text();
|
||||||
throw new Error(`Token request failed (${tokenResp.status}): ${body}`);
|
throw new Error(`Token request failed (${tokenResp.status}): ${body}`);
|
||||||
@@ -189,7 +226,15 @@ export class RegistryCopy {
|
|||||||
fetchOptions.duplex = 'half'; // Required for streaming body in Node
|
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}`}`;
|
||||||
|
delete this.tokenCache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,11 +365,11 @@ export class RegistryCopy {
|
|||||||
putHeaders['Authorization'] = `Bearer ${token}`;
|
putHeaders['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const putResp = await fetch(putUrl, {
|
const putResp = await this.fetchWithRetry(putUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: putHeaders,
|
headers: putHeaders,
|
||||||
body: blobData,
|
body: blobData,
|
||||||
});
|
}, 300_000);
|
||||||
|
|
||||||
if (!putResp.ok) {
|
if (!putResp.ok) {
|
||||||
const body = await putResp.text();
|
const body = await putResp.text();
|
||||||
|
|||||||
Reference in New Issue
Block a user