AuthErrorMapper
Supabase::Rails::Web::AuthErrorMapper — translates Supabase::Auth::Errors::* into the gem-stable Supabase::Rails::AuthError surface with deterministic code and HTTP status.
Supabase::Rails::Web::AuthErrorMapper is the single place every Supabase::Auth::Errors::* exception from supabase-rb is translated into the gem's stable Supabase::Rails::AuthError surface. The output carries a code (one of nine constants on AuthError) and an HTTP status that controllers, the Rack middleware, and any host error-handling code can match on without inspecting upstream exception classes.
You will use this module any time you call Supabase::Auth::Client methods from custom code — sign_in_with_otp, verify_otp, update_user, reauthenticate, etc. The mapper is also called internally by the gem-shipped controllers (SessionsController, PasswordsController, OauthController, OtpController, RegistrationsController) so flash messages and JSON error bodies stay consistent. The module is documented here because (a) the dispatch is case/when against Supabase::Auth::Errors::* ancestors and ordering matters, (b) AuthApiError's status-based subdispatch is the only mapping with a numeric branch, and (c) the 503 outcomes are the ones operators most need to recognise during incidents.
# Typical use from a custom controller.
def reauthenticate
client = Supabase::Rails::Web::AuthClientFactory.build(request)
client.reauthenticate
head :no_content
rescue ::Supabase::Auth::Errors::AuthError => e
err = Supabase::Rails::Web::AuthErrorMapper.translate(e)
Rails.logger.warn("Reauth failed: [#{err.code}] #{err.message}")
render json: { code: err.code, message: err.message }, status: err.status
endAuthErrorMapper.translate(err)
| Returns | Visibility |
|---|---|
Supabase::Rails::AuthError with stable code and status | Public, module-method. |
| Argument | Type | Notes |
|---|---|---|
err | Supabase::Auth::Errors::AuthError or any StandardError | The upstream exception. Non-Supabase exceptions fall through the default branch and become AUTH_GENERIC_ERROR / 500. |
translate never raises. The message is extracted via err.respond_to?(:message) ? err.message.to_s : err.to_s, so an exception with a nil message produces an AuthError with message: "" rather than blowing up.
The translation table
The case dispatch covers every documented Supabase::Auth::Errors::* class. The first match wins, so the table also shows the visiting order — leaf classes before their ancestors, since the upstream hierarchy nests some subclasses several levels deep.
Upstream supabase-rb error class | AuthError::CODE | HTTP status | Notes |
|---|---|---|---|
AuthInvalidCredentialsError | INVALID_CREDENTIALS | 401 | Wrong email/password, invalid OTP. |
AuthInvalidJwtError | INVALID_CREDENTIALS | 401 | A token-shape problem upstream (rare in :web mode — JWT verify happens inside the gem via JWT.verify). |
AuthSessionMissing | SESSION_MISSING | 401 | The upstream client tried to act on a session that didn't exist (e.g. reauthenticate without a current session). |
AuthWeakPassword | WEAK_PASSWORD | 422 | Sign-up or password update rejected by Supabase's strength rules. |
AuthPKCEError | PKCE_ERROR | 400 | OAuth round-trip failed — verifier mismatch, missing state, etc. |
AuthRetryableError | AUTH_RETRYABLE | 503 | Upstream signalled "try again" (rate-limited, brief unavailability). |
AuthApiError 4xx | AUTH_API_ERROR | preserved 4xx | The upstream HTTP status flows through verbatim. |
AuthApiError 5xx | AUTH_UPSTREAM_ERROR | 503 | Every 5xx collapses to 503 — your app is healthy, Supabase is degraded. |
AuthUnknownError | AUTH_GENERIC_ERROR | 500 | Upstream raised something the supabase-rb hierarchy doesn't classify. |
Any other StandardError | AUTH_GENERIC_ERROR | 500 | Defensive — out-of-band errors (e.g. Timeout::Error) hit the default branch. |
Why AuthApiError is split by status
Supabase::Auth::Errors::AuthApiError is a single class that wraps any non-2xx HTTP response from /auth/v1/*. The mapper sub-dispatches on err.status:
def map_api_error(message, status)
if status.is_a?(Integer) && status >= 500
AuthError.new(message, AuthError::AUTH_UPSTREAM_ERROR, 503)
else
upstream = status.is_a?(Integer) ? status : 400
AuthError.new(message, AuthError::AUTH_API_ERROR, upstream)
end
end- 4xx preserves the upstream status. A
422 Unprocessable Entityfrom Supabase becomesAuthError(AUTH_API_ERROR, 422)— caller error semantics are kept intact. - 5xx collapses to
503 Service Unavailable. The caller (your app) is fine; the dependency is degraded. Returning the literal502/504Supabase reported would be confusing — operators reading your app's metrics would assume your upstream returned that status. - Non-Integer status (defensive: if the upstream ever yields a
nilstatus) is treated as400.
The 5xx-to-503 collapse is also what keeps the CookieCredentialStrategy refresh path's REFRESH_UNAVAILABLE semantics consistent — :transient is the same shape regardless of whether Supabase reported 500, 502, 503, or 504.
Ordering of the case/when
The dispatch is case/when err, which uses Class#=== (i.e. is_a?). Five of the leaf classes (AuthRetryableError, AuthSessionMissing, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthWeakPassword) inherit from CustomAuthError < AuthError; AuthApiError, AuthPKCEError, and AuthUnknownError inherit directly from AuthError. None of them inherit from each other, so the order within those two leaf groups is cosmetic — but the leaf-before-ancestor rule still matters because:
AuthApiErroris the only class that needs status-based sub-dispatch — placing it beforeAuthErrorensures it isn't swallowed by a too-broad ancestor.- A future upstream class added under one of these branches will need to be placed in the dispatch before its parent. The mapper is small enough that this is easy to spot in PR review, but the module docstring calls it out explicitly to keep the rule in mind.
Code constants
Supabase::Rails::AuthError defines nine code constants — eight surface through this mapper and one (INVALID_REDIRECT) is gem-side only (see RedirectValidator):
| Constant | String value | Produced by |
|---|---|---|
AuthError::INVALID_CREDENTIALS | "INVALID_CREDENTIALS" | AuthInvalidCredentialsError, AuthInvalidJwtError, and gem-internal JWT verify failures. |
AuthError::SESSION_MISSING | "SESSION_MISSING" | AuthSessionMissing. |
AuthError::WEAK_PASSWORD | "WEAK_PASSWORD" | AuthWeakPassword. |
AuthError::PKCE_ERROR | "PKCE_ERROR" | AuthPKCEError. |
AuthError::AUTH_RETRYABLE | "AUTH_RETRYABLE" | AuthRetryableError. |
AuthError::AUTH_API_ERROR | "AUTH_API_ERROR" | AuthApiError (4xx). |
AuthError::AUTH_UPSTREAM_ERROR | "AUTH_UPSTREAM_ERROR" | AuthApiError (5xx). |
AuthError::AUTH_GENERIC_ERROR | "AUTH_ERROR" | AuthUnknownError and any unrecognised StandardError. |
AuthError::REFRESH_UNAVAILABLE | "REFRESH_UNAVAILABLE" | Not produced by translate — raised directly by CookieCredentialStrategy for the cookie-refresh transient path. |
AuthError::CREATE_SUPABASE_CLIENT_ERROR | "CREATE_SUPABASE_CLIENT_ERROR" | Not produced by translate — raised by Supabase::Rails.build_context_result on Supabase::SupabaseException. |
AuthError::INVALID_REDIRECT | "INVALID_REDIRECT" | Not produced by translate — raised by RedirectValidator. |
The AUTH_GENERIC_ERROR constant uses the string "AUTH_ERROR" (without the GENERIC_ infix) — a small naming asymmetry that matters when matching on the string in tests.
Match on the constants (e.code == AuthError::INVALID_CREDENTIALS), not the strings — the strings are stable, but the constants are clearer and let your IDE jump to definition.
Errors that don't go through the mapper
Three AuthError paths bypass translate because they are raised directly by the gem rather than by the upstream client:
| Error | Raised by | Why bypass |
|---|---|---|
AuthError.invalid_credentials (manual constructor) | Supabase::Rails::JWT.verify and Core.verify_credentials | The gem fails JWT verification before talking to upstream — there's no upstream exception to translate. |
AuthError.refresh_unavailable | CookieCredentialStrategy | The strategy already classified the upstream error as :transient and produces the 503 directly. Sending it back through the mapper would duplicate work. |
AuthError.invalid_redirect(uri) | RedirectValidator | Open-redirect rejection is a gem-side check that never sees an upstream call. |
AuthError.session_missing (manual constructor) | Authentication concern, when terminate_session is called without an active session | Same — gem-side, no upstream call. |
The mapper is exclusively for upstream-to-stable-AuthError translation. Matching on e.code works the same regardless of which path produced the error.
How errors surface
:web mode
Most upstream errors during sign-in / sign-up / OAuth / OTP / password-reset are caught inside the gem-shipped controllers and rendered as a flash message + redirect back to the form. The mapper is what populates the code you see logged via the [supabase.rails.sign_in_failure] code=<MAPPED_CODE> line — <MAPPED_CODE> is the string value of AuthError::CODE.
For the cookie-refresh transient path specifically, the middleware renders a JSON 503 body ({ "message": "...", "code": "REFRESH_UNAVAILABLE" }) — see CookieCredentialStrategy → Anonymous downgrade vs. JSON 503.
:api mode
The middleware translates upstream errors via the mapper and renders the JSON body:
{ "message": "Invalid credentials", "code": "INVALID_CREDENTIALS" }with the mapped status. Clients can match on code to display a localised message.
Examples
Translating an upstream error in a custom controller
def update_password
client = Supabase::Rails::Web::AuthClientFactory.build(request)
client.update_user(password: params[:password])
flash.notice = "Password updated."
redirect_to account_path
rescue ::Supabase::Auth::Errors::AuthError => e
err = Supabase::Rails::Web::AuthErrorMapper.translate(e)
case err.code
when Supabase::Rails::AuthError::WEAK_PASSWORD
flash.alert = "Password is too weak. #{err.message}"
when Supabase::Rails::AuthError::SESSION_MISSING
flash.alert = "Please sign in again to change your password."
else
flash.alert = "Couldn't update password (#{err.code})."
end
redirect_to edit_account_path
endTranslating a non-Supabase error
err = Supabase::Rails::Web::AuthErrorMapper.translate(Timeout::Error.new("oops"))
err.code # => "AUTH_ERROR"
err.status # => 500
err.message # => "oops"The default branch catches anything not in the upstream hierarchy and produces a 500 AUTH_GENERIC_ERROR — a safe fallback that won't accidentally surface as a 401 or a credential-shaped error to the user.
Mapping an AuthApiError 5xx to 503
upstream = ::Supabase::Auth::Errors::AuthApiError.new("Bad gateway", status: 502)
err = Supabase::Rails::Web::AuthErrorMapper.translate(upstream)
err.code # => "AUTH_UPSTREAM_ERROR"
err.status # => 503Your app's 502 is now a 503 — the right semantic for "we couldn't talk to Supabase" rather than "we couldn't talk to ourselves".
Mapping a 4xx through to its upstream status
upstream = ::Supabase::Auth::Errors::AuthApiError.new("Email taken", status: 422)
err = Supabase::Rails::Web::AuthErrorMapper.translate(upstream)
err.code # => "AUTH_API_ERROR"
err.status # => 422The 4xx is the caller's problem — preserving it lets the host UI distinguish 409 Conflict from 422 Unprocessable Entity without parsing the message.
What this module does not do
- It does not log. Logging is the caller's responsibility (the gem-shipped controllers log a redacted
[supabase.rails.sign_in_failure]line; the middleware logs throughSupabase::Rails::Logging). The mapper is pure — call it, then decide how loudly to react. - It does not raise. A nil message, a missing status, or a non-Supabase exception all produce a valid
AuthError. The mapper is safe to call fromrescueblocks without furtherbegin/rescue. - It does not produce gem-side error codes.
INVALID_REDIRECT,REFRESH_UNAVAILABLE, andCREATE_SUPABASE_CLIENT_ERRORare raised directly by their respective modules — see the table in Errors that don't go through the mapper. - It does not localise messages. The
messageis whatever the upstream said. Hosts that want localised error text should match onerr.codeand look up an I18n key.
See also
- Web mode overview — the table of upstream-to-
AuthErrormappings repeated in the section's overview. CookieCredentialStrategy— the refresh path, which producesREFRESH_UNAVAILABLEwithout going through this mapper.AuthClientFactory— the per-request client factory; pairbuildwithAuthErrorMapper.translatewhen calling upstream methods from custom code.RedirectValidator— the source ofINVALID_REDIRECT, the one upstream-independentAuthErrorcode.- Authentication — the controller-side
AuthErrorrescue patterns. - JWT verification → Errors raised — the JWT verifier's own
AuthErrorpaths.
AuthClientFactory
Supabase::Rails::Web::AuthClientFactory — constructs the per-request Supabase::Auth::Client with auto_refresh_token disabled, PKCE flow, and request-scoped storage, cached on request.env.
Controllers
Built-in controllers under Supabase::Rails::* and the supabase_authentication_routes routing DSL.