Skip to content

auth-js: handleError discards the response body for 5xx, so every server error reports message: "{}" (regression in 2.108.2) #2585

Description

@developsly1

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 525529 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. 502504 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

  • Follow our Code of Conduct
  • Read the Contributing Guidelines.
  • Read the docs.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • Make sure this is a Supabase JS Library issue and not an issue with the Supabase platform.
  • Check that this is a concrete bug.
  • The provided reproduction is a minimal reproducible example of the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    auth-jsRelated to the auth-js library.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions