From 1ea1aa20149c594b4cf65d887bfdf79fff3e6566 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:50:18 +0300 Subject: [PATCH] feat(auth): support throwing errors instead of returning them --- packages/core/auth-js/src/GoTrueClient.ts | 70 +++++++++++++++++-- packages/core/auth-js/src/lib/types.ts | 5 ++ .../core/auth-js/test/GoTrueClient.test.ts | 34 +++++++++ 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index 341d35ce0..e7d4656ec 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -166,6 +166,7 @@ const DEFAULT_OPTIONS: Omit< flowType: 'implicit', debug: false, hasCustomAuthorizationHeader: false, + throwOnError: false, } async function lockNoOp(name: string, acquireTimeout: number, fn: () => Promise): Promise { @@ -252,6 +253,7 @@ export default class GoTrueClient { protected lock: LockFunc protected lockAcquired = false protected pendingInLock: Promise[] = [] + protected throwOnError: boolean /** * Used to broadcast state change events to other tabs listening. @@ -297,6 +299,7 @@ export default class GoTrueClient { this.detectSessionInUrl = settings.detectSessionInUrl this.flowType = settings.flowType this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader + this.throwOnError = settings.throwOnError if (settings.lock) { this.lock = settings.lock @@ -464,6 +467,7 @@ export default class GoTrueClient { return { error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { error } } @@ -494,6 +498,7 @@ export default class GoTrueClient { const { data, error } = res if (error || !data) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error: error } } const session: Session | null = data.session @@ -507,6 +512,7 @@ export default class GoTrueClient { return { data: { user, session }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -572,6 +578,7 @@ export default class GoTrueClient { const { data, error } = res if (error || !data) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error: error } } @@ -586,6 +593,7 @@ export default class GoTrueClient { return { data: { user, session }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -636,9 +644,12 @@ export default class GoTrueClient { const { data, error } = res if (error) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } else if (!data || !data.session || !data.user) { - return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() } + const invalidTokenError = new AuthInvalidTokenResponseError() + if (this.throwOnError) throw invalidTokenError + return { data: { user: null, session: null }, error: invalidTokenError } } if (data.session) { await this._saveSession(data.session) @@ -654,6 +665,7 @@ export default class GoTrueClient { } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } throw error @@ -842,6 +854,7 @@ export default class GoTrueClient { return { data: { ...data }, error } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -1030,6 +1043,7 @@ export default class GoTrueClient { return { data: { ...data }, error } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -1078,6 +1092,7 @@ export default class GoTrueClient { return { data: { ...data, redirectType: redirectType ?? null }, error } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null, redirectType: null }, error } } @@ -1107,11 +1122,14 @@ export default class GoTrueClient { const { data, error } = res if (error) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } else if (!data || !data.session || !data.user) { + const invalidTokenError = new AuthInvalidTokenResponseError() + if (this.throwOnError) throw invalidTokenError return { data: { user: null, session: null }, - error: new AuthInvalidTokenResponseError(), + error: invalidTokenError, } } if (data.session) { @@ -1121,6 +1139,7 @@ export default class GoTrueClient { return { data, error } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } throw error @@ -1168,6 +1187,7 @@ export default class GoTrueClient { }, redirectTo: options?.emailRedirectTo, }) + if (this.throwOnError && error) throw error return { data: { user: null, session: null }, error } } if ('phone' in credentials) { @@ -1182,11 +1202,13 @@ export default class GoTrueClient { channel: options?.channel ?? 'sms', }, }) + if (this.throwOnError && error) throw error return { data: { user: null, session: null, messageId: data?.message_id }, error } } throw new AuthInvalidCredentialsError('You must provide either an email or phone number.') } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -1216,11 +1238,13 @@ export default class GoTrueClient { }) if (error) { + if (this.throwOnError) throw error throw error } - if (!data) { - throw new Error('An error occurred on token verification.') + const tokenVerificationError = new Error('An error occurred on token verification.') + if (this.throwOnError) throw tokenVerificationError + throw tokenVerificationError } const session: Session | null = data.session @@ -1237,6 +1261,7 @@ export default class GoTrueClient { return { data: { user, session }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -1286,6 +1311,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -1318,10 +1344,12 @@ export default class GoTrueClient { headers: this.headers, jwt: session.access_token, }) + if (this.throwOnError && error) throw error return { data: { user: null, session: null }, error } }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } throw error @@ -1345,6 +1373,7 @@ export default class GoTrueClient { }, redirectTo: options?.emailRedirectTo, }) + if (this.throwOnError && error) throw error return { data: { user: null, session: null }, error } } else if ('phone' in credentials) { const { phone, type, options } = credentials @@ -1356,6 +1385,7 @@ export default class GoTrueClient { gotrue_meta_security: { captcha_token: options?.captchaToken }, }, }) + if (this.throwOnError && error) throw error return { data: { user: null, session: null, messageId: data?.message_id }, error } } throw new AuthInvalidCredentialsError( @@ -1363,6 +1393,7 @@ export default class GoTrueClient { ) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } throw error @@ -1608,6 +1639,7 @@ export default class GoTrueClient { const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { + if (this.throwOnError) throw error return { data: { session: null }, error } } @@ -1675,6 +1707,7 @@ export default class GoTrueClient { await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`) } + if (this.throwOnError) throw error return { data: { user: null }, error } } @@ -1734,7 +1767,10 @@ export default class GoTrueClient { jwt: session.access_token, xform: _userResponse, }) - if (userError) throw userError + if (userError) { + if (this.throwOnError) throw userError + throw userError + } session.user = data.user as User await this._saveSession(session) await this._notifyAllSubscribers('USER_UPDATED', session) @@ -1742,6 +1778,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null }, error } } @@ -1789,6 +1826,7 @@ export default class GoTrueClient { currentSession.refresh_token ) if (error) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error: error } } @@ -1816,6 +1854,7 @@ export default class GoTrueClient { return { data: { user: session.user, session }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { session: null, user: null }, error } } @@ -1857,6 +1896,7 @@ export default class GoTrueClient { const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error: error } } @@ -1868,6 +1908,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } @@ -2002,6 +2043,7 @@ export default class GoTrueClient { return { data: { session, redirectType: params.type }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { session: null, redirectType: null }, error } } @@ -2050,6 +2092,7 @@ export default class GoTrueClient { return await this._useSession(async (result) => { const { data, error: sessionError } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { error: sessionError } } const accessToken = data.session?.access_token @@ -2064,6 +2107,7 @@ export default class GoTrueClient { (error.status === 404 || error.status === 401 || error.status === 403) ) ) { + if (this.throwOnError) throw error return { error } } } @@ -2194,6 +2238,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } @@ -2219,6 +2264,7 @@ export default class GoTrueClient { return { data: { identities: data.user.identities ?? [] }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -2271,6 +2317,7 @@ export default class GoTrueClient { return { data: { provider: credentials.provider, url: data?.url }, error: null } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { provider: credentials.provider, url: null }, error } } throw error @@ -2320,6 +2367,7 @@ export default class GoTrueClient { return { data, error } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { user: null, session: null }, error } } throw error @@ -2355,6 +2403,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -2401,6 +2450,7 @@ export default class GoTrueClient { this._debug(debugName, 'error', error) if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: { session: null, user: null }, error } } throw error @@ -3008,6 +3058,7 @@ export default class GoTrueClient { return await this._useSession(async (result) => { const { data: sessionData, error: sessionError } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { data: null, error: sessionError } } @@ -3018,6 +3069,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -3035,6 +3087,7 @@ export default class GoTrueClient { return await this._useSession(async (result) => { const { data: sessionData, error: sessionError } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { data: null, error: sessionError } } @@ -3065,6 +3118,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -3085,6 +3139,7 @@ export default class GoTrueClient { return await this._useSession(async (result) => { const { data: sessionData, error: sessionError } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { data: null, error: sessionError } } @@ -3147,6 +3202,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -3172,6 +3228,7 @@ export default class GoTrueClient { return await this._useSession(async (result) => { const { data: sessionData, error: sessionError } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { data: null, error: sessionError } } @@ -3236,6 +3293,7 @@ export default class GoTrueClient { }) } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error @@ -3311,6 +3369,7 @@ export default class GoTrueClient { error: sessionError, } = result if (sessionError) { + if (this.throwOnError) throw sessionError return { data: null, error: sessionError } } if (!session) { @@ -3497,6 +3556,7 @@ export default class GoTrueClient { } } catch (error) { if (isAuthError(error)) { + if (this.throwOnError) throw error return { data: null, error } } throw error diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 734d8ef0a..78a7ba6ef 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -107,6 +107,11 @@ export type GoTrueClientOptions = { * @experimental */ hasCustomAuthorizationHeader?: boolean + /** + * If there is an error with the query, throwOnError will reject the promise by + * throwing the error instead of returning it as part of a successful response. + */ + throwOnError?: boolean } const WeakPasswordReasons = ['length', 'characters', 'pwned'] as const diff --git a/packages/core/auth-js/test/GoTrueClient.test.ts b/packages/core/auth-js/test/GoTrueClient.test.ts index ca345c002..8711e7731 100644 --- a/packages/core/auth-js/test/GoTrueClient.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.test.ts @@ -3059,3 +3059,37 @@ describe('Branch Coverage Improvements', () => { }) }) }) + +describe('GoTrueClient with throwOnError option', () => { + const store = memoryLocalStorageAdapter() + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + storageKey: 'test-storage-key', + autoRefreshToken: false, + persistSession: true, + storage: { + ...store, + }, + throwOnError: true, // test that the client throws errors + }) + + test('signUp() should throw an error when throwOnError is true', async () => { + const { email, password } = mockUserCredentials() + + await expect(client.signUp({ email, password: '' })).rejects.toThrow() + }) + + test('signInWithPassword() should throw an error when throwOnError is true', async () => { + const { email, password } = mockUserCredentials() + + await expect(client.signInWithPassword({ email, password: '' })).rejects.toThrow() + }) + + test('updateUser() should throw an error when throwOnError is true', async () => { + const { email, password } = mockUserCredentials() + + await client.signUp({ email, password }) + + await expect(client.updateUser({ email: 'invalid-email' })).rejects.toThrow() + }) +})