Skip to content

fix(core): return a code instead of an empty response for an empty failure body - #134

Merged
Bccorb merged 1 commit into
mainfrom
fix/empty-upstream-failure-body
Jul 30, 2026
Merged

fix(core): return a code instead of an empty response for an empty failure body#134
Bccorb merged 1 commit into
mainfrom
fix/empty-upstream-failure-body

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #125.

Problem

The auth-flow routes forward the auth API's failure body rather than interpreting it. An empty body left nothing to forward, so the caller got a bare 4xx with no content:

Route Before
POST /login (empty)
GET /oauth/providers (empty)
PATCH /admin/users/:id {"error":"admin_request_failed"}

The proxy routes already had a fallback via readUpstreamFailure, so the two families disagreed on precisely the case where the caller has least to go on. Downstream, toSeamlessAuthError in seamless-auth-react reads the body for a message, finds none, and uses the per-call fallback string, so the user sees a generic failure whether the session expired, the request was rate limited, or the API was down.

Fix

readPassthroughFailure(data, fallback?) forwards the body when there is one and returns a code when there is not. The auth-flow handlers now route through it.

Verified end to end against a running adapter:

empty 400 body         400 {"error":"upstream_error"}
normal error body      400 {"error":"account_locked"}
oauth sibling code     400 {"error":"oauth_profile_error","code":"oauth_email_not_verified"}
rate-limit text body   400 {"message":"Too many requests."}

Only the first line changes. A present body is still forwarded untouched, including the top-level code the React SDK reads to tell OAuth failures apart, which is the constraint that shaped the errorBody contract in the first place.

Note the rate-limit row: a non-JSON body is not empty here, because authFetch already turns plain text into { message: text }. Only a genuinely empty body hits the fallback.

New exports

readPassthroughFailure and UPSTREAM_ERROR_CODE, both from @seamless-auth/core.

Tests

  • packages/core/tests/passthroughFailure.test.js: four body shapes forwarded untouched, four empty-ish values falling back, a caller-supplied fallback, and the invariant that a result never carries both an errorBody and an errorCode.
  • One case added to the Express failureWireFormat guard, so the wire shape is locked alongside the others.

Checks

pnpm build clean. pnpm test passes: 372 tests across the three packages.

…ilure body

The auth-flow routes forward the API's failure body rather than interpreting
it, and an empty body left nothing to forward: the caller got a bare 4xx with
no content, and the React SDK fell back to a generic message with no way to
tell an expired session from a rate limit from an upstream outage.

The proxy routes already had a fallback, so the two families disagreed on the
one case where the caller had least to go on.

readPassthroughFailure forwards a body when there is one and returns
upstream_error when there is not. A present body is still untouched,
including the top-level code the SDK reads for OAuth failures.

Closes #125
@Bccorb
Bccorb merged commit 82fc15a into main Jul 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A 4xx with an empty upstream body returns an empty response on passthrough routes

1 participant