Retry Discord API 5xx errors and handle non-JSON error bodies (#87)

Cloudflare edge errors (5xx, edge-level 429s) return plain-text bodies
like "upstream connect error..." or "error code: 1015" that crash a
naive response.json() call. Add a tryParseJson helper and use it for
all error paths so we surface the actual HTTP status/body instead of a
JSON parse exception.

Also add exponential backoff retry for 5xx responses, matching the
existing 429 retry behavior.

🏠 Remote-Dev: homespace
This commit is contained in:
Den Delimarsky 2026-03-23 15:53:17 -07:00 • committed by GitHub
commit 40c29e305a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -30,6 +30,16 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Cloudflare 5xx and edge-level 429s return plain-text bodies ("upstream connect
// error...", "error code: 1015") that crash a naive response.json().
function tryParseJson<T>(text: string): T | undefined {
try {
return JSON.parse(text) as T;
} catch {
return undefined;
}
}
async function discordFetch<T>(
token: string,
endpoint: string,
@ -49,13 +59,15 @@ async function discordFetch<T>(
});
if (response.status === 429) {
const body = (await response.json()) as DiscordRateLimitResponse;
const text = await response.text();
const body = tryParseJson<DiscordRateLimitResponse>(text);
const retryAfterSec = body?.retry_after ?? 1;
// Linearly increasing jitter de-syncs the thundering herd when many
// resources refresh in parallel and all receive the same retry_after.
const jitterMs = Math.random() * 1000 * (attempt + 1);
const retryAfterMs = Math.ceil(body.retry_after * 1000) + jitterMs;
const retryAfterMs = Math.ceil(retryAfterSec * 1000) + jitterMs;
lastError = new Error(
`Discord API rate limited on ${endpoint} (retry_after=${body.retry_after}s, global=${body.global})`
`Discord API rate limited on ${endpoint} (retry_after=${retryAfterSec}s, global=${body?.global ?? false})`
);
if (attempt < maxRetries) {
await sleep(retryAfterMs);
@ -64,9 +76,24 @@ async function discordFetch<T>(
throw lastError;
}
if (response.status >= 500 && response.status < 600) {
const text = await response.text();
lastError = new Error(`Discord API ${response.status} on ${endpoint}: ${text.slice(0, 200)}`);
if (attempt < maxRetries) {
await sleep(2 ** attempt * 500 + Math.random() * 1000);
continue;
}
throw lastError;
}
if (!response.ok) {
const error = (await response.json()) as DiscordApiError;
throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
const text = await response.text();
const error = tryParseJson<DiscordApiError>(text);
throw new Error(
error
? `Discord API error: ${error.message} (code: ${error.code})`
: `Discord API ${response.status} on ${endpoint}: ${text.slice(0, 200)}`
);
}
// Handle 204 No Content