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
- Print res.status and the response body before throwing (temporarily log text via readBoundedResponse) to identify the PostgREST error detail
- Verify SUPABASE_URL and service-role key env vars are set and belong to the same project
- Run publish with --dry-run first to confirm channel and key setup before writing
- 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
- 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
- Always validate with --dry-run before a real publish
- Confirm service-role (not anon) credentials for write operations
- Pin the expected table names and check Supabase schema migrations
- Run from an environment with network access to the Supabase host
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
- supabase-http-${res.status}
- building bundle fetch client failed
- bundle fetch failed with HTTP status
- bundle fetch request failed
- bundle redirects may not change URL scheme
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)