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 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.
|
||||
* Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
|
||||
|
|
@ -29,12 +60,14 @@ class RegistryClient {
|
|||
}
|
||||
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 = '';
|
||||
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('timeout', () => {
|
||||
|
|
@ -133,12 +166,14 @@ class RegistryClient {
|
|||
}
|
||||
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 = '';
|
||||
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('timeout', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue