fix(installer): include URL, API message, and rate-limit info in HTTP errors
Non-2xx responses previously yielded bare `HTTP 403`. Now surface the request URL, GitHub's JSON error message (or body snippet), X-RateLimit-Reset when quota is exhausted, and Retry-After. Turns a mystery 403 into 'rate limit exhausted; resets at 2026-04-15T18:00:00Z' — the difference between 'try GITHUB_TOKEN' and a wild goose chase.
This commit is contained in:
parent
8c3301fc49
commit
e2e032cd96
|
|
@ -1,6 +1,37 @@
|
||||||
const https = require('node:https');
|
const https = require('node:https');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a rich Error from a non-2xx response. Includes the URL, the GitHub
|
||||||
|
* JSON error message (or a truncated body snippet), rate-limit reset time,
|
||||||
|
* and Retry-After — anything present that would help a user recover.
|
||||||
|
*/
|
||||||
|
function buildHttpError(url, res, body) {
|
||||||
|
const parts = [`HTTP ${res.statusCode} ${url}`];
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
if (parsed.message) parts.push(parsed.message);
|
||||||
|
if (parsed.documentation_url) parts.push(`(see ${parsed.documentation_url})`);
|
||||||
|
} catch {
|
||||||
|
const snippet = body.slice(0, 200).trim();
|
||||||
|
if (snippet) parts.push(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = res.headers['x-ratelimit-remaining'];
|
||||||
|
const reset = res.headers['x-ratelimit-reset'];
|
||||||
|
if (remaining === '0' && reset) {
|
||||||
|
parts.push(`rate limit exhausted; resets at ${new Date(Number(reset) * 1000).toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryAfter = res.headers['retry-after'];
|
||||||
|
if (retryAfter) parts.push(`retry after ${retryAfter}`);
|
||||||
|
|
||||||
|
return new Error(parts.join(' — '));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared HTTP client for fetching registry data from GitHub.
|
* Shared HTTP client for fetching registry data from GitHub.
|
||||||
* Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
|
* Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
|
||||||
|
|
@ -29,12 +60,14 @@ class RegistryClient {
|
||||||
}
|
}
|
||||||
return this.fetch(res.headers.location, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
return this.fetch(res.headers.location, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
||||||
}
|
}
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk) => (data += chunk));
|
res.on('data', (chunk) => (data += chunk));
|
||||||
res.on('end', () => resolve(data));
|
res.on('end', () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
return reject(buildHttpError(url, res, data));
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('timeout', () => {
|
.on('timeout', () => {
|
||||||
|
|
@ -133,12 +166,14 @@ class RegistryClient {
|
||||||
}
|
}
|
||||||
return this._fetchWithHeaders(res.headers.location, headers, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
return this._fetchWithHeaders(res.headers.location, headers, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
||||||
}
|
}
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk) => (data += chunk));
|
res.on('data', (chunk) => (data += chunk));
|
||||||
res.on('end', () => resolve(data));
|
res.on('end', () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
return reject(buildHttpError(url, res, data));
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('timeout', () => {
|
.on('timeout', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue