{"record":{"id":"af8ed1a74cd5295a","repo":"Hmbown/CodeWhale","slug":"supabase-http-res-status","errorCode":"supabase-http-${res.status}","errorMessage":"supabase-http-${res.status}","messagePattern":"supabase-http-(.+?)","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/lib/cloud-facts.ts","lineNumber":269,"sourceCode":"export async function fetchCurrentRow(channel: string, env: CloudFactsEnv, opts: ResolveOptions = {}): Promise<FactsCurrentRow | null> {\n  if (!isValidChannel(channel)) throw new Error(\"invalid-channel\");\n  const key = env.SUPABASE_PUBLISHABLE_KEY;\n  let base: URL;\n  try {\n    base = new URL(env.SUPABASE_URL ?? \"\");\n    if (base.protocol !== \"https:\" || base.username || base.password || base.search || base.hash || !key || !isPublishableKey(key)) throw new Error();\n  } catch { throw new Error(\"supabase-not-configured\"); }\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUPABASE_TIMEOUT_MS);\n  try {\n    const url = new URL(`${base.href.replace(/\\/+$/, \"\")}/rest/v1/facts_current`);\n    url.search = new URLSearchParams({ channel: `eq.${channel}`, scope: \"eq.global\", select: \"channel,release_id,facts_version,schema_version,envelope_version,applies_to,key_id,payload_b64,sig_b64,sigs,payload_sha256,published_at,not_after\", limit: \"1\" }).toString();\n    const res = await (opts.fetchImpl ?? fetch)(url, {\n      headers: { apikey: key!, Authorization: `Bearer ${key}`, Accept: \"application/json\" },\n      signal: controller.signal,\n      redirect: \"error\",\n    });\n    if (!res.ok) { await res.body?.cancel(); throw new Error(`supabase-http-${res.status}`); }\n    const bytes = await readBoundedBody(res, MAX_ENVELOPE_BYTES);\n    const rows: unknown = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n    if (!Array.isArray(rows) || rows.length > 1) throw new Error(\"supabase-bad-row\");\n    if (rows.length === 0) return null;\n    if (!isObject(rows[0]) || !isEnvelope(envelopeFromRow(rows[0] as unknown as FactsCurrentRow))) throw new Error(\"supabase-bad-row\");\n    return rows[0] as unknown as FactsCurrentRow;\n  } finally { clearTimeout(timer); }\n}\n\nasync function kvGet(env: CloudFactsEnv, channel: string): Promise<unknown> {\n  if (!env.CURATED_KV) return null;\n  try {\n    const body = await env.CURATED_KV.get(`${KV_PREFIX}${channel}`, \"stream\");\n    if (!body) return null;\n    const bytes = await readBoundedBody({ body, headers: new Headers() }, MAX_ENVELOPE_BYTES);\n    return JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n  } catch { return null; }\n}","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/lib/cloud-facts.ts#L251-L287","documentation":"After fetching /rest/v1/facts_current, fetchCurrentRow requires a 2xx response. Any non-ok status is converted to `supabase-http-${res.status}` (after cancelling the body to free the connection), so callers can branch on the exact HTTP status embedded in the message — e.g. 401/403 for auth problems, 404 for a missing table, 5xx for Supabase outages.","triggerScenarios":"Supabase REST returns 401/403 (bad or revoked publishable key, missing apikey header), 404 (facts_current table absent), 429 (rate limit), or 5xx (platform error) to the built request.","commonSituations":"Rotated the anon key in the Supabase dashboard without updating the worker secret; RLS policies denying reads; facts_current table not yet created/migrated; Supabase incident producing 502/503.","solutions":["Read the status in the message and fix accordingly: 401/403 → refresh SUPABASE_PUBLISHABLE_KEY; 404 → create/restore facts_current table; 429 → back off and respect rate limits.","Re-sync the publishable key secret after any key rotation.","Verify the project's database migrations ran so facts_current exists with the selected columns.","Retry 5xx with backoff; treat 4xx as configuration problems that retrying will not fix."],"exampleFix":"// before\nconst row = await fetchCurrentRow(channel, env);\n\n// after\ncatch (e) {\n  const m = /supabase-http-(\\d+)/.exec(e.message);\n  if (m && m[1].startsWith('5')) return retryWithBackoff();\n  if (m && m[1] === '429') return rateLimitedFallback();\n}","handlingStrategy":"retry","validationCode":"// Cannot pre-validate an HTTP status; validate config instead:\nconst ok = !!env.SUPABASE_PUBLISHABLE_KEY && /^https:\\/\\/[^/?#]+$/.test(env.SUPABASE_URL ?? '');","typeGuard":null,"tryCatchPattern":"try {\n  return await fetchCurrentRow(channel, env);\n} catch (e) {\n  const m = /supabase-http-(\\d+)/.exec(e.message);\n  if (!m) throw e;\n  const status = Number(m[1]);\n  if (status === 429 || status >= 500) return retryWithBackoff(3);\n  throw new Error(`Supabase ${status}: check key/table`);\n}","preventionTips":["Monitor Supabase status and set up alerts for 5xx spikes.","Rotate keys via a documented process that updates worker secrets in the same change.","Ensure DB migrations create facts_current before deploys that read it.","Cache the last good row so 429/5xx can be served stale."],"tags":["network","http","supabase"],"backgroundTag":"http-error-response","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}