Hmbown/CodeWhale · error · Error

PostgREST request failed

Error message

PostgREST request failed (HTTP ${res.status})

What it means

The publish script's postgrest() helper throws this whenever the Supabase/PostgREST HTTP response status is not ok (res.ok false). It cancels the body to free the connection and raises a generic error containing only the status code. Any 4xx/5xx from PostgREST (auth failure, bad table, RLS denial, constraint violation) surfaces here.

Solutions

  1. Print res.status and the response body before throwing (temporarily log text via readBoundedResponse) to identify the PostgREST error detail
  2. Verify SUPABASE_URL and service-role key env vars are set and belong to the same project
  3. Run publish with --dry-run first to confirm channel and key setup before writing
  4. Check the facts_channel row exists globally for the envelope's channel (error 689) and facts_key conflicts are handled by the Prefer: resolution=ignore-duplicates header
  5. Check Supabase project status/logs for RLS or constraint errors

Example fix

// before
if (!res.ok) { await res.body?.cancel(); throw new Error(`PostgREST request failed (HTTP ${res.status})`); }
// after
if (!res.ok) {
  const detail = await res.text().catch(() => "");
  await res.body?.cancel();
  throw new Error(`PostgREST request failed (HTTP ${res.status}): ${detail.slice(0, 500)}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!process.env.SUPABASE_URL || !process.env.SUPABASE_KEY) throw new Error('SUPABASE_URL/SUPABASE_KEY must be set');
const ping = await fetch(`${process.env.SUPABASE_URL}/rest/v1/facts_channel?select=id&limit=1`, { headers: { Authorization: `Bearer ${process.env.SUPABASE_KEY}` } });
if (!ping.ok) throw new Error(`precheck failed: HTTP ${ping.status}`);

Try / catch

try {
  await publish(envelope);
} catch (e) {
  if (String(e.message).includes('PostgREST request failed')) {
    console.error('Supabase rejected the request; check env vars, RLS, table names, and run --dry-run first');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling publish (POST facts_key/facts_release), emit-sql key lookup, or the channel existence GET when Supabase returns a non-2xx status: wrong SUPABASE_URL/table names, missing or expired service-role key, RLS policy blocking the anon key, or a 409 duplicate key_id.

Common situations: Publishing with an anon key instead of service role; facts_channel table missing or renamed; env vars (SUPABASE_URL/SUPABASE_KEY) unset or pointing at a different project; inserting a facts_key row that already exists without resolution=ignore-duplicates; network/proxy outage returning 502.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/62e360c0e78ace58. Report an issue: GitHub.

Appendix: source

Thrown at web/scripts/facts-publish.mjs:466

  refuseUnderCi();
  const url = process.env.SUPABASE_URL;
  const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;
  if (!url || !key) throw new Error("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required");
  const endpoint = new URL(url);
  if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error("invalid Supabase endpoint");
  const res = await fetch(`${url.replace(/\/$/, "")}/rest/v1/${path}`, {
    method,
    signal: AbortSignal.timeout(30_000),
    redirect: "error",
    headers: {
      apikey: key,
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      ...(prefer ? { Prefer: prefer } : {}),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) { await res.body?.cancel(); throw new Error(`PostgREST request failed (HTTP ${res.status})`); }
  const text = await readBoundedResponse(res);
  return text ? JSON.parse(text) : null;
}

export async function readBoundedResponse(response, maxBytes = MAX_ENVELOPE_BYTES) {
  const length = response.headers.get("content-length");
  if (length !== null && (!/^\d+$/.test(length) || Number(length) > maxBytes)) {
    await response.body?.cancel();
    throw new Error("response exceeds size limit or has invalid length");
  }
  if (!response.body) return "";
  const reader = response.body.getReader();
  const chunks = [];
  let size = 0;
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;

View on GitHub (pinned to 433685b202)