Hmbown/CodeWhale · error · Error
invalid Supabase endpoint
Error message
invalid Supabase endpoint
What it means
postgrest parses SUPABASE_URL with new URL() and enforces a clean https origin: protocol must be https and username, password, search (query), and hash must all be empty. Anything else is rejected to stop the service-role key from being sent to a malformed or attacker-influenced endpoint.
Solutions
- Set SUPABASE_URL to the bare https project origin, e.g. https://<projectref>.supabase.co with no query, fragment, credentials, or trailing path
- Strip any ?... or #... that was pasted along with the URL and re-export the variable
- If you need a non-standard endpoint (self-hosted proxy), it must still be https with a clean origin; fix the proxy URL rather than the env value
Example fix
// before SUPABASE_URL=https://xyz.supabase.co/rest/v1?apikey=abc // after SUPABASE_URL=https://xyz.supabase.co
Defensive patterns
Strategy: validation
Validate before calling
function assertCleanSupabaseUrl(raw) {
const u = new URL(raw); // throws separately on unparseable URLs
const bad = u.protocol !== "https:" || u.username || u.password || u.search || u.hash;
if (bad) throw new Error(`SUPABASE_URL must be a bare https origin, got: ${raw}`);
return u.origin;
}
assertCleanSupabaseUrl(process.env.SUPABASE_URL); Type guard
function isCleanHttpsOrigin(raw) {
try {
const u = new URL(raw);
return u.protocol === "https:" && !u.username && !u.password && !u.search && !u.hash;
} catch {
return false;
}
} Try / catch
try {
await publishFacts(envelope);
} catch (err) {
if (err.message === "invalid Supabase endpoint") {
console.error("SUPABASE_URL must be a clean https origin (no query, fragment, credentials, or http) — fix the env value");
process.exit(1);
}
throw err;
} Prevention
- Store the bare project origin (https://<ref>.supabase.co) in env; never paste URLs with ?apikey=... or #...
- Add a startup check that validates SUPABASE_URL with the same rules before any network call
- For self-hosted endpoints, keep https and a clean origin; put paths/params in code, not the env value
When it happens
Trigger: Calling postgrest when SUPABASE_URL is http://, contains a query string or fragment (e.g. a pasted URL with ?key=... or #/), embeds userinfo (user:pass@host), is not a valid URL at all (new URL throws separately), or includes a trailing path with parameters.
Common situations: A URL was copied from the Supabase dashboard including query/hash decorations; a staging proxy used http://; the env var accidentally holds the REST base with ?apikey=... appended; a typo like https:/host (single slash) fails URL parsing.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- bundle URLs may not include credentials
- fleet alert URL from
- plain http is only allowed for loopback hosts; use https
- The Codewhale service returned an unsafe verification URL
- agent action=claim widens an enforced write scope, and the…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/d0863bfb99fc3b57.
Report an issue: GitHub.
Appendix: source
Thrown at web/scripts/facts-publish.mjs:453
` on conflict (key_id) do nothing;`,
`insert into public.facts_release (channel_id, facts_version, schema_version, envelope_version, applies_to, key_id, payload_b64, sig_b64, sigs, payload, published_at, not_after, published_by, notes)`,
` select c.id, ${envelope.facts_version}, ${envelope.schema_version}, ${envelope.envelope}, ${sqlLiteral(envelope.applies_to)}, ${sqlLiteral(envelope.key_id)},`,
` ${sqlLiteral(envelope.payload_b64)}, ${sqlLiteral(envelope.sig_b64)}, ${sqlLiteral(JSON.stringify(envelope.sigs ?? []))}::jsonb,`,
` ${sqlLiteral(payloadJson)}::jsonb, ${sqlLiteral(envelope.published_at)}::timestamptz, ${sqlLiteral(check.payload.not_after ?? null)}::timestamptz,`,
` ${sqlLiteral(publishedBy)}, ${sqlLiteral(notes)}`,
` from public.facts_channel c where c.scope = 'global' and c.slug = ${sqlLiteral(envelope.channel)};`,
"commit;",
"",
].join("\n");
}
async function postgrest(path, { method = "GET", body, prefer } = {}) {
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) {View on GitHub (pinned to 433685b202)