windmill-labs/windmill · error
${what}: ${supabaseErrorMessage(body) || res.statusText}
Error message
${what}: ${supabaseErrorMessage(body) || res.statusText} What it means
unwrap is the shared response checker for Supabase Management API calls made during workspace provisioning. Any non-OK HTTP response is thrown as `<what>: <supabase-provided message or statusText>`. A 401 additionally clears the stored OAuth token so the Connect screen reappears. It surfaces the upstream Supabase error to the user with context about which call failed.
Solutions
- If the message says unauthorized (401), click Connect again to re-authenticate — the stale token was already cleared.
- Generate a fresh Supabase access token with the required scopes (org read/write, project create) and reconnect.
- Retry after a delay if the status is 429 rate limit.
- Check status.supabase.com if the error is 5xx, then retry.
- Verify the Supabase account actually has an organization (and quota) before provisioning.
Defensive patterns
Strategy: try-catch
Validate before calling
if (!oauthStoreValue) { showConnectScreen(); return }
const testRes = await fetch(`${base}/api/oauth/supabase_orgs`, { headers: headers(token) })
if (testRes.status === 401) { oauthStore.set(undefined); showConnectScreen(); return } Type guard
function isSupabaseErrorBody(b: unknown): b is { message?: string; error?: string } {
return typeof b === 'object' && b !== null && ('message' in b || 'error' in b)
} Try / catch
try {
const orgs = await listSupabaseOrgs(token)
} catch (e) {
if (/401|unauthorized/i.test(e.message)) promptReconnect()
else if (/429/.test(e.message)) scheduleRetry()
else showError(e.message)
} Prevention
- Regenerate Supabase access tokens regularly and reconnect before long provisioning sessions.
- Create tokens with the full org/project management scopes.
- Check Supabase status page before large provisioning runs.
When it happens
Trigger: Any call routed through unwrap (listSupabaseOrgs, listSupabaseProjects, createSupabaseProject, pooler configs fetch) when Supabase answers 401 (expired/revoked access token), 403 (no org permission), 404, 429 (rate limit), or 5xx.
Common situations: Expired Supabase personal access token in oauthStore after sitting on the page; token created without the org/project scopes needed; Supabase rate limiting org creation; Supabase API outage.
Related errors
- Failed to complete GitHub app installation
- ApiError with mapped HTTP status message (e.g. "Not Found"…
- await res.text()
- body.error || res.statusText
- body || res.statusText
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/8cd3e4e7991d80ec.
Report an issue: GitHub.
Appendix: source
Thrown at frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts:73
{ code: 'eu-west-3', label: 'West EU (Paris)' },
{ code: 'ap-southeast-1', label: 'Southeast Asia (Singapore)' },
{ code: 'ap-northeast-1', label: 'Northeast Asia (Tokyo)' }
]
export const DEFAULT_SUPABASE_REGION = 'eu-central-1'
function headers(token: string): HeadersInit {
return { 'Content-Type': 'application/json', 'X-Supabase-Token': token }
}
async function unwrap(res: Response, what: string): Promise<any> {
if (!res.ok) {
// Supabase access tokens are short-lived while `oauthStore` lasts as long as the tab, so
// a stale one otherwise leaves every caller "authorized" and unable to reach the button
// that would fix it. Forgetting it here is what puts Connect back on screen.
if (res.status === 401) oauthStore.set(undefined)
const body = await res.text()
throw new Error(`${what}: ${supabaseErrorMessage(body) || res.statusText}`)
}
return res.json()
}
/**
* Supabase answers with `{ message }` or `{ error }` and occasionally plain text.
* Surfacing the raw body puts a JSON blob in front of the user, so unwrap it to
* the sentence inside.
*/
export function supabaseErrorMessage(body: string): string {
try {
const parsed = JSON.parse(body)
return parsed?.message ?? parsed?.error ?? parsed?.msg ?? body
} catch {
return body
}
}
View on GitHub (pinned to e474e8803c)