export type HeadersLike = Headers | Record | undefined | null; const RETRY_AFTER_HINT = "retry-after-ms="; export function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string { const message = error instanceof Error ? error.message : JSON.stringify(error); if (message.includes(RETRY_AFTER_HINT)) { return message; } const retryAfterMs = getRetryAfterMsFromHeaders(headers ?? getHeadersFromError(error)); if (retryAfterMs === undefined) { return message; } return `${message} ${RETRY_AFTER_HINT}${retryAfterMs}`; } export function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined { if (!headers) return undefined; const retryAfter = parseRetryAfterHeader(getHeaderValue(headers, "retry-after")); const resetMs = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset-ms"), "ms"); const resetSeconds = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset"), "s"); const candidates = [retryAfter, resetMs, resetSeconds].filter((value): value is number => value !== undefined); if (candidates.length === 0) return undefined; return Math.max(...candidates); } export function getHeadersFromError(error: unknown): HeadersLike { if (!error || typeof error !== "object") return undefined; const record = error as { headers?: unknown; response?: { headers?: unknown }; cause?: unknown }; const direct = extractHeaders(record.headers) ?? extractHeaders(record.response?.headers); if (direct) return direct; if (record.cause) return getHeadersFromError(record.cause); return undefined; } function extractHeaders(value: unknown): HeadersLike { if (!value) return undefined; if (value instanceof Headers) return value; if (typeof value === "object") return value as Record; return undefined; } function getHeaderValue(headers: Headers | Record, name: string): string | undefined { if (headers instanceof Headers) { const value = headers.get(name); return value ?? undefined; } const target = name.toLowerCase(); for (const [key, value] of Object.entries(headers)) { if (key.toLowerCase() === target && typeof value === "string") { return value; } } return undefined; } function parseRetryAfterHeader(value: string | undefined): number | undefined { if (!value) return undefined; const trimmed = value.trim(); if (!trimmed) return undefined; const numeric = Number(trimmed); if (Number.isFinite(numeric)) { if (numeric <= 0) return undefined; return Math.ceil(numeric * 1000); } const dateMs = Date.parse(trimmed); if (!Number.isNaN(dateMs)) { const delay = dateMs - Date.now(); return delay > 0 ? Math.ceil(delay) : undefined; } return undefined; } function parseResetHeader(value: string | undefined, unit: "ms" | "s"): number | undefined { if (!value) return undefined; const numeric = Number(value); if (!Number.isFinite(numeric) || numeric <= 0) return undefined; const nowMs = Date.now(); let targetMs: number | undefined; if (unit === "ms") { if (numeric > 1e12) { targetMs = numeric; } else if (numeric > 1e9) { targetMs = numeric * 1000; } else { return Math.ceil(numeric); } } else { if (numeric > 1e12) { targetMs = numeric; } else if (numeric > 1e9) { targetMs = numeric * 1000; } else { return Math.ceil(numeric * 1000); } } if (targetMs <= nowMs) return undefined; return Math.ceil(targetMs - nowMs); }