Problem Details
DbError as RFC 9457 application/problem+json, with RFC 6750 challenges.
Server adapters answer errors with RFC 9457
Problem Details. The DbError fields become extension members, so clients
keep the structured error:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://bettersupabase.com/problems/conflict",
"title": "Conflict",
"status": 409,
"kind": "conflict",
"detail": "duplicate key value violates unique constraint …",
"instance": "/customers",
"code": "23505",
"constraint": "customers_organization_id_kvk_key",
"columns": ["organization_id", "kvk"]
}import { fromProblem, problemResponse } from "better-supabase";
if (!result.ok)
return problemResponse(result.error, { instance: url.pathname });
// client side
const error = fromProblem(await response.json()); // back to a DbErrorKind-specific details become extension members next to code: constraint
and columns for conflicts, column for not_null, and required (aal1 or
aal2) when a guard's aal rejects the session,
scopes when a guard's scopes rejects a delegated token
(code: "INSUFFICIENT_SCOPE"), code: "ANONYMOUS_USER" when a guard turns
away an anonymous user because allow lacks 'anonymous', retryAfter for rate_limited, which
also sets the Retry-After header, meter, limit and retryAfter for
quota_exceeded, and maxAffected for max_affected.
When an authorizer refuses a declared
permission, the forbidden error carries permission (the permission key)
and one of two codes. PERMISSION_DENIED means the authorizer denied it,
failed, or isn't set; detail holds its reason. APPROVAL_REQUIRED means
the action needs an approval first, and approval ({ "id": "..." }) names
the pending approval when the authorizer gives one:
{
"type": "https://bettersupabase.com/problems/forbidden",
"title": "Forbidden",
"status": 403,
"kind": "forbidden",
"detail": "invoices.update needs an approval",
"code": "APPROVAL_REQUIRED",
"permission": "invoices.update",
"approval": { "id": "apr_9" }
}Safe by default
Messages of unexpected, network and invalid_request errors can contain
SQL or hostnames, so they are left out unless you pass expose: true (for
example in development). Validation errors keep their issues.
Your own error format
format rewrites the body, for apps whose errors use their own type URIs
or codes. It gets the Problem Details and the DbError; the status and the
Retry-After and WWW-Authenticate headers stay as they are.
const toAppError = (problem: ProblemDetails, error: DbError) => ({
type: `https://errors.example.com/${error.code ?? problem.kind}`,
title: problem.title,
status: problem.status,
detail: problem.detail,
});
problemResponse(error, { instance, format: toAppError });The blocks that answer HTTP requests take the same function as problem:
createIdempotency, createWebhookInbox, createIncomingWebhooks, rateLimited,
and the drainRoute, deliverRoute and relayRoute options.
401 responses
A 401 carries an RFC 6750
challenge: WWW-Authenticate: Bearer realm="supabase", error="invalid_token",
without error when no credentials were sent. Set the realm with realm.
A 403 for a missing OAuth scope carries the RFC 6750 insufficient_scope
challenge, naming the scopes the route needs:
WWW-Authenticate: Bearer realm="supabase", error="insufficient_scope", scope="customers:read".
Last updated on