From d2bc9aabfa776395a6ac7c0624dec7ccab48fdba Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Mon, 20 Apr 2026 13:13:41 -0500 Subject: [PATCH] fix(drift): retry transient OpenAPI spec fetch failures A 502 Bad Gateway from the spec endpoint was aborting the scheduled drift-detection workflow and triggering noisy Slack alerts. Wrap the spec fetch in an exponential-backoff retry (4 attempts, jittered, per-attempt 30s timeout) that retries on 5xx/429 and network errors while still failing fast on 4xx. --- scripts/detect-drift.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/scripts/detect-drift.ts b/scripts/detect-drift.ts index 8b8325e..d6b2461 100644 --- a/scripts/detect-drift.ts +++ b/scripts/detect-drift.ts @@ -36,16 +36,46 @@ function normalizeEndpoint(endpoint: string): string { .replace(/\/$/, ''); } +async function fetchWithRetry(url: string, maxAttempts = 4): Promise { + const base = 500; + let lastError: unknown; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(30_000) }); + if (response.ok) return response; + if (response.status >= 500 || response.status === 429) { + lastError = new Error(`${response.status} ${response.statusText}`); + } else { + return response; + } + } catch (err) { + lastError = err; + } + if (attempt < maxAttempts - 1) { + const delay = base * 2 ** attempt + Math.floor(Math.random() * base); + const message = lastError instanceof Error ? lastError.message : String(lastError); + console.error( + `Fetch attempt ${attempt + 1}/${maxAttempts} for ${url} failed: ${message}. Retrying in ${delay}ms...` + ); + await new Promise((r) => setTimeout(r, delay)); + } + } + const message = lastError instanceof Error ? lastError.message : String(lastError); + throw new Error(`Failed to fetch ${url} after ${maxAttempts} attempts: ${message}`); +} + async function fetchSpecEndpoints(): Promise> { - const response = await fetch(OPENAPI_SPEC_URL); + const response = await fetchWithRetry(OPENAPI_SPEC_URL); if (!response.ok) { throw new Error(`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`); } - const spec = parseYaml(await response.text()) as Record; + const spec = parseYaml(await response.text()) as { + paths?: Record>; + }; const endpoints = new Map(); for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { - for (const method of Object.keys(pathItem as object)) { + for (const method of Object.keys(pathItem)) { if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) { const key = `${method.toUpperCase()} ${normalizeEndpoint(path)}`; endpoints.set(key, `${method.toUpperCase()} ${path}`);