Describe the bug
Since auth-js 2.108.2, every server-side auth error arrives with message: "{}". The response body — which GoTrue fills in correctly — is discarded before it is ever read.
handleError returns early for any status in NETWORK_ERROR_CODES before parsing the body, passing the raw Response to _getErrorMessage. That helper looks for msg / message / error_description / error as own properties. A Response has none of them, so it falls through to JSON.stringify(err) — which yields the string "{}", because a Response has no own enumerable properties at all.
packages/core/auth-js/src/lib/fetch.ts (2.111.0, lines 79–95):
export async function handleError(error: unknown) {
if (!looksLikeFetchResponse(error)) {
throw new AuthRetryableFetchError(_getErrorMessage(error), 0)
}
if (NETWORK_ERROR_CODES.includes(error.status)) {
// status in 500...599 range - server had an error, request might be retryed.
throw new AuthRetryableFetchError(_getErrorMessage(error), error.status) // <-- `error` is the Response
}
let data: any
try {
data = await error.json() // <-- unreachable for any status in the list
} catch (e) {
throw new AuthUnknownError(_getErrorMessage(e), e)
}
...
This is a regression. #2436 added 500, 501 and 525–529 to NETWORK_ERROR_CODES, first released in 2.108.2:
| version |
NETWORK_ERROR_CODES |
result for a 500 with a JSON body |
| 2.108.1 |
[502, 503, 504, 520…524, 530] |
AuthApiError / 500 / "Error sending confirmation email" ✅ |
| 2.108.2+ |
[500, 501, 502 … 530] |
AuthRetryableFetchError / 500 / "{}" ❌ |
I confirmed both rows by running the reproduction below against the installed build with only that array changed.
The problem is not the widened list — treating a 500 as retryable is right, and I am not suggesting reverting #2436. The problem is that 500 is the status GoTrue uses for application-level failures that come with a perfectly good JSON body, and the early return throws that body away. 502–504 and the Cloudflare codes rarely carry one, which is why the flaw was invisible until 500 joined the list.
Why it matters
"{}" is indistinguishable from a transport failure, so an application cannot separate:
- a mistyped address, so the mail bounced — the user can fix this
- SMTP misconfigured or rate-limited — the operator can fix this
- the auth service is genuinely down — nobody can, retry later
All three arrive as message: "{}", status: 500. In our case a signup with a two-letter typo in the domain (…@gmail.c) was reported to the user as a database connectivity problem, because that is the only honest thing an app can say about "{}". The server had stated exactly what was wrong; the client discarded it.
This likely accounts for a tail of reports where people hit a bare AuthRetryableFetchError: {} with no way to diagnose it (e.g. #1292, supabase/auth-js#837).
Library affected
auth-js
Steps to reproduce
Self-contained — no Supabase project or credentials needed. A local server returns exactly the body GoTrue sends when a confirmation email cannot be delivered:
// repro.mjs — node repro.mjs
import { createServer } from 'node:http'
import { createClient } from '@supabase/supabase-js'
const BODY = JSON.stringify({
code: 500,
error_code: 'unexpected_failure',
msg: 'Error sending confirmation email',
})
const server = createServer((req, res) => {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(BODY)
})
await new Promise((r) => server.listen(0, '127.0.0.1', r))
const supabase = createClient(`http://127.0.0.1:${server.address().port}`, 'any-anon-key')
const { error } = await supabase.auth.signUp({
email: 'someone@example.com',
password: 'correct horse battery staple',
})
console.log('server sent :', BODY)
console.log('error.name :', error?.name)
console.log('error.status :', error?.status)
console.log('error.message:', JSON.stringify(error?.message))
server.close()
Run it once on 2.111.0, then again on 2.108.1, to see the regression.
Expected: error.message is "Error sending confirmation email" — the text the server sent, which _getErrorMessage finds under msg as soon as it is given the parsed body rather than the Response.
Actual on 2.108.2+: error.message is "{}".
Suggested fix
Parse the body first and keep the NETWORK_ERROR_CODES check for the classification. The branch only needs to move; what it throws is unchanged.
export async function handleError(error: unknown) {
if (!looksLikeFetchResponse(error)) {
throw new AuthRetryableFetchError(_getErrorMessage(error), 0)
}
let data: any
try {
data = await error.json()
} catch (e) {
// A 5xx frequently has no JSON body at all (a gateway HTML page). That is
// still a retryable network error, not an unknown one.
if (NETWORK_ERROR_CODES.includes(error.status)) {
throw new AuthRetryableFetchError(_getErrorMessage(error), error.status)
}
throw new AuthUnknownError(_getErrorMessage(e), e)
}
if (NETWORK_ERROR_CODES.includes(error.status)) {
// status in 500...599 range - server had an error, request might be retryed.
throw new AuthRetryableFetchError(_getErrorMessage(data), error.status)
}
// ...unchanged from here
}
The error class and the status are unchanged, so nothing branching on AuthRetryableFetchError or on .status is affected, and the session-preservation behaviour from #2436 and #2430 still holds.
The re-check inside the catch is the load-bearing part. Without it, a 5xx carrying an HTML gateway page becomes AuthUnknownError and loses its retryable classification — the exact case the current ordering protects. I verified both paths by applying this patch to the installed 2.111.0 build:
| case |
before |
after |
| 500 + JSON body |
AuthRetryableFetchError / 500 / "{}" |
AuthRetryableFetchError / 500 / "Error sending confirmation email" |
| 502 + HTML body, no JSON |
AuthRetryableFetchError / 502 |
AuthRetryableFetchError / 502 — unchanged |
I'm happy to submit a PR with this plus tests covering both rows, if the approach looks right.
System Info
System:
OS: Windows 11 10.0.26200
CPU: (16) x64 11th Gen Intel(R) Core(TM) i9-11900KF @ 3.50GHz
Memory: 10.39 GB / 63.87 GB
Binaries:
Node: 24.13.1
npm: 11.8.0
npmPackages:
@supabase/ssr: ^0.5.2 => 0.5.2
@supabase/supabase-js: ^2.45.4 => 2.111.0
Also reproduced against a hosted Supabase project from the Cloudflare Workers runtime, with the same result.
Used Package Manager
npm
Logs
Click to expand!
# @supabase/supabase-js 2.111.0 (auth-js 2.111.0)
server sent : {"code":500,"error_code":"unexpected_failure","msg":"Error sending confirmation email"}
error.name : AuthRetryableFetchError
error.status : 500
error.message: "{}"
# same script, NETWORK_ERROR_CODES reverted to the pre-#2436 list
server sent : {"code":500,"error_code":"unexpected_failure","msg":"Error sending confirmation email"}
error.name : AuthApiError
error.status : 500
error.message: "Error sending confirmation email"
Validations
Describe the bug
Since
auth-js2.108.2, every server-side auth error arrives withmessage: "{}". The response body — which GoTrue fills in correctly — is discarded before it is ever read.handleErrorreturns early for any status inNETWORK_ERROR_CODESbefore parsing the body, passing the rawResponseto_getErrorMessage. That helper looks formsg/message/error_description/erroras own properties. AResponsehas none of them, so it falls through toJSON.stringify(err)— which yields the string"{}", because aResponsehas no own enumerable properties at all.packages/core/auth-js/src/lib/fetch.ts(2.111.0, lines 79–95):This is a regression. #2436 added
500,501and525–529toNETWORK_ERROR_CODES, first released in 2.108.2:NETWORK_ERROR_CODES[502, 503, 504, 520…524, 530]AuthApiError/ 500 /"Error sending confirmation email"✅[500, 501, 502 … 530]AuthRetryableFetchError/ 500 /"{}"❌I confirmed both rows by running the reproduction below against the installed build with only that array changed.
The problem is not the widened list — treating a 500 as retryable is right, and I am not suggesting reverting #2436. The problem is that
500is the status GoTrue uses for application-level failures that come with a perfectly good JSON body, and the early return throws that body away.502–504and the Cloudflare codes rarely carry one, which is why the flaw was invisible until500joined the list.Why it matters
"{}"is indistinguishable from a transport failure, so an application cannot separate:All three arrive as
message: "{}",status: 500. In our case a signup with a two-letter typo in the domain (…@gmail.c) was reported to the user as a database connectivity problem, because that is the only honest thing an app can say about"{}". The server had stated exactly what was wrong; the client discarded it.This likely accounts for a tail of reports where people hit a bare
AuthRetryableFetchError: {}with no way to diagnose it (e.g. #1292, supabase/auth-js#837).Library affected
auth-js
Steps to reproduce
Self-contained — no Supabase project or credentials needed. A local server returns exactly the body GoTrue sends when a confirmation email cannot be delivered:
Run it once on
2.111.0, then again on2.108.1, to see the regression.Expected:
error.messageis"Error sending confirmation email"— the text the server sent, which_getErrorMessagefinds undermsgas soon as it is given the parsed body rather than theResponse.Actual on 2.108.2+:
error.messageis"{}".Suggested fix
Parse the body first and keep the
NETWORK_ERROR_CODEScheck for the classification. The branch only needs to move; what it throws is unchanged.The error class and the status are unchanged, so nothing branching on
AuthRetryableFetchErroror on.statusis affected, and the session-preservation behaviour from #2436 and #2430 still holds.The re-check inside the
catchis the load-bearing part. Without it, a 5xx carrying an HTML gateway page becomesAuthUnknownErrorand loses its retryable classification — the exact case the current ordering protects. I verified both paths by applying this patch to the installed 2.111.0 build:AuthRetryableFetchError/ 500 /"{}"AuthRetryableFetchError/ 500 /"Error sending confirmation email"AuthRetryableFetchError/ 502AuthRetryableFetchError/ 502 — unchangedI'm happy to submit a PR with this plus tests covering both rows, if the approach looks right.
System Info
System: OS: Windows 11 10.0.26200 CPU: (16) x64 11th Gen Intel(R) Core(TM) i9-11900KF @ 3.50GHz Memory: 10.39 GB / 63.87 GB Binaries: Node: 24.13.1 npm: 11.8.0 npmPackages: @supabase/ssr: ^0.5.2 => 0.5.2 @supabase/supabase-js: ^2.45.4 => 2.111.0Also reproduced against a hosted Supabase project from the Cloudflare Workers runtime, with the same result.
Used Package Manager
npm
Logs
Click to expand!
Validations