feat(errors): add self-identifying errors with hints and diagnostics - #130
feat(errors): add self-identifying errors with hints and diagnostics#130johnstonmatt wants to merge 6 commits into
Conversation
Nearly every failure returned `{ message: "Invalid credentials", code:
"INVALID_CREDENTIALS" }` — naming neither the cause nor the library it
came from.
Provenance. All errors now share a `SupabaseServerError` base carrying
`source: "@supabase/server"`, a `[@supabase/server]` message prefix (the
convention `deprecation.ts` already used for warnings), a `docs` link to
the matching `docs/error-handling.md` section, an optional `hint`, and
non-sensitive `details`. `toJSON()` renders the wire payload and is picked
up by `JSON.stringify`, so logging no longer yields `{}`. One
`errorResponse()` helper renders it everywhere, repeating the code in an
`x-supabase-server-error` header and adding that to
`Access-Control-Expose-Headers` so cross-origin callers can read it.
Top-level `message` and `code` are unchanged, so existing consumers and
the adapters keep working.
Diagnosis. `verifyUserJwt` now returns *why* a token failed instead of
`null`, and the mode chain records why each mode fell through, so the
final error names the real cause: `MISSING_CREDENTIALS`,
`INVALID_API_KEY`, `INVALID_JWT`, plus `JWKS_NOT_CONFIGURED`,
`JWKS_FETCH_FAILED` and `NO_KEYS_CONFIGURED` for states where no request
could ever have succeeded. `INVALID_CREDENTIALS` stays exported as the
fallback. Hints cover the mistakes people actually make — a secret key
sent to a publishable-only endpoint, a legacy anon/service_role key, an
`Authorization` header without the `Bearer` scheme, a JWT with no `kid`,
an expired token, a JWKS from the wrong project.
The middleware that answer directly get the same treatment rather than
their own hand-rolled bodies: `withClaims` / `withRequiredClaims` report
`MISSING_JWKS`, `MISSING_CREDENTIALS`, `INVALID_JWT`;
`withPostgresClient` / `withPostgresAdminClient` report
`MISSING_CONNECTION_STRING` and a catalogued `UNSUPPORTED_ROLE`.
`details` never carries key values or token payloads: API keys are
reported by prefix format, named keys by name, JWTs by `alg`/`kid` only.
Note: server misconfiguration now surfaces as 500 rather than 401. A
missing or unreachable JWKS, or an auth mode no configured key can match,
are not the caller's fault.
`hint`, `docs`, and `details` are written for whoever is building against the endpoint, and not everyone wants them on the wire. `errors.detailed` (default `true`) reduces the body to `code` and `message` alone. Provenance survives the trim: `message` keeps its `[@supabase/server]` prefix, and the code is still sent as the `x-supabase-server-error` header — so the error stays identifiable without the `source` field. Response-only. The HTTP status is unaffected and the error object keeps `hint`, `docs`, and `details` in full, so `createSupabaseContext` callers and the framework adapters see everything. Documented as a verbosity control rather than a security boundary — `code` and `message` still name the failure specifically. Formatting the response by hand via `createSupabaseContext` remains the way to disclose nothing.
0d530f1 to
517dbae
Compare
commit: |
|
|
||
| --- | ||
|
|
||
| ## Error Code Constants |
There was a problem hiding this comment.
Maybe we could add a "Possible Causes" or similar column to this table? So is easier for agents/users to think of a root case. Does not need to be very detailed, but for example, something like: "apikey or Authorization headers are missing"
There was a problem hiding this comment.
we can do that! do you think it is needed in addition to the other places this PR adds info, like docs/error-handling.md ?
Review feedback on #130: the top-level code was `MISSING_CREDENTIALS` even when a credential had arrived, just the wrong kind. `received. authorization: 'api-key'` and the hint carried the diagnosis, but `errors: { detailed: false }` strips both — leaving a caller who is demonstrably sending a key staring at a bare `MISSING_CREDENTIALS`. That mode makes the code the only thing a caller can rely on, so it has to be true standing alone. `UNUSABLE_CREDENTIAL` (401) now covers "a credential arrived that no accepted mode can use", partitioning the space exactly against `MISSING_CREDENTIALS` ("nothing arrived"). It has two shapes, named in the `message` so the diagnosis survives the trim: - wrong kind: an `sb_*` API key in the Authorization header - unreadable: wrong scheme, wrong casing, bare value, empty token The unreadable shapes had the same defect and are fixed with it — a `Basic` or lowercase-`bearer` header is not a missing credential either. Classification moves into one shared `diagnoseAuthorizationHeader`, since only the raw header separates "sent nothing" from "sent something unreadable" and both `verifyAuth` and the `withRequiredClaims` gate need that distinction. Previously the gate could not make it at all, so the two disagreed on every scheme case. A parity matrix over all six header shapes now pins gate and `withSupabase({ auth: 'user' })` to the same status and code.
|
@johnstonmatt One follow-up, since |
Review feedback on #130: `supabase-js` sends the publishable key in both the `apikey` and `Authorization` headers, so an unauthenticated browser call to an `auth: 'user'` endpoint arrives with a key in each slot. The `apikey !== 'absent'` branch in `explainFallthrough` was read first, so the caller got `INVALID_API_KEY` — "check you are pointing at the right Supabase project" — for a key that was never going to be looked up. With `errors: { detailed: false }` the code is all they get, and it sent them hunting for a key mismatch that does not exist. `INVALID_API_KEY` means "matched none of the configured keys", which only says something when a mode was doing that lookup. It is now gated on an attempted `publishable` / `secret` mode; where no mode reads keys, a key in either header is `UNUSABLE_CREDENTIAL` — not wrong, just the wrong kind of credential. The apikey-header-only case had the same defect and is fixed with it: "matched no key configured for auth mode(s): "user"" described a lookup that never happened. The new diagnosis is shared as `apiKeyOnUserOnlyEndpoint`, so the `withRequiredClaims` gate stops answering with "API keys belong in the `apikey` header" for callers who already sent it there — that gate only ever accepts a user JWT, so moving the key would not help. It keeps the parity the gate is built for: an identical request, worded identically from both paths. `ApiKeyInAuthorizationHeader` still covers the case where the advice is right — a mixed `['user', 'publishable']` endpoint with a key in `Authorization` alone.
…ential error handling
Nearly every failure used to return the same generic
{ message: "Invalid credentials", code: "INVALID_CREDENTIALS" }, naming neither the cause nor the source library. All errors now identify themselves and explain what to do next.SupabaseServerError, a shared base carryingsource, a[@supabase/server]message prefix, adocslink, an optionalhint, and non-sensitivedetails; add a singleerrorResponse()helper that renders it everywhere (with anx-supabase-server-errorheader exposed via CORS), while keeping top-levelmessage/codeunchanged for existing consumersverifyUserJwtand the auth mode chain report the specific reason a request failed instead of a generic invalid-credentials fallback, addingMISSING_CREDENTIALS,INVALID_API_KEY,INVALID_JWT,JWKS_NOT_CONFIGURED,JWKS_FETCH_FAILED, andNO_KEYS_CONFIGURED, and surfacing server misconfiguration as 500 instead of 401withClaims,withRequiredClaims,withPostgresClient, andwithPostgresAdminClientthe same specific, hinted errors instead of hand-rolled bodieserrors: { detailed: false }towithSupabaseto trim response bodies to justcodeandmessagewhile leaving status, the error header, and the in-process error object untouchedUNUSABLE_CREDENTIALout fromMISSING_CREDENTIALSso a credential that arrived but couldn't be used (wrong kind or unreadableAuthorizationheader) is distinguishable from one that never arrived, via a shareddiagnoseAuthorizationHeaderclassifier used by bothverifyAuthand thewithRequiredClaimsgatedetailsnever carries secret material — API keys are reported by prefix format and named keys by name onlydocs/error-handling.mdand updatedocs/api-reference.md/docs/postgres.mdto document the new error classes, codes, and response-trimming behavior