diff --git a/.changeset/fluffy-otters-whisper.md b/.changeset/fluffy-otters-whisper.md new file mode 100644 index 00000000000..863b608875b --- /dev/null +++ b/.changeset/fluffy-otters-whisper.md @@ -0,0 +1,5 @@ +--- +'@firebase/app-check': patch +--- + +Improve error handling in AppCheck. The publicly-exported `getToken()` will now throw `internalError` strings it was previously ignoring. diff --git a/packages/app-check/src/api.ts b/packages/app-check/src/api.ts index e6897320be1..a4dd87a4e77 100644 --- a/packages/app-check/src/api.ts +++ b/packages/app-check/src/api.ts @@ -209,6 +209,9 @@ export async function getToken( if (result.error) { throw result.error; } + if (result.internalError) { + throw result.internalError; + } return { token: result.token }; } diff --git a/packages/app-check/src/errors.ts b/packages/app-check/src/errors.ts index c6f088b371b..ca5a60aed6b 100644 --- a/packages/app-check/src/errors.ts +++ b/packages/app-check/src/errors.ts @@ -27,6 +27,7 @@ export const enum AppCheckError { STORAGE_GET = 'storage-get', STORAGE_WRITE = 'storage-set', RECAPTCHA_ERROR = 'recaptcha-error', + INITIAL_THROTTLE = 'initial-throttle', THROTTLED = 'throttled' } @@ -54,7 +55,8 @@ const ERRORS: ErrorMap = { [AppCheckError.STORAGE_WRITE]: 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.', [AppCheckError.RECAPTCHA_ERROR]: 'ReCAPTCHA error.', - [AppCheckError.THROTTLED]: `Requests throttled due to {$httpStatus} error. Attempts allowed again after {$time}` + [AppCheckError.INITIAL_THROTTLE]: `{$httpStatus} error. Attempts allowed again after {$time}`, + [AppCheckError.THROTTLED]: `Requests throttled due to previous {$httpStatus} error. Attempts allowed again after {$time}` }; interface ErrorParams { @@ -66,6 +68,7 @@ interface ErrorParams { [AppCheckError.STORAGE_OPEN]: { originalErrorMessage?: string }; [AppCheckError.STORAGE_GET]: { originalErrorMessage?: string }; [AppCheckError.STORAGE_WRITE]: { originalErrorMessage?: string }; + [AppCheckError.INITIAL_THROTTLE]: { time: string; httpStatus: number }; [AppCheckError.THROTTLED]: { time: string; httpStatus: number }; } diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index 1e43a5e7e21..5d6b88f1c32 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -163,7 +163,7 @@ describe('internal api', () => { const error = new Error('oops, something went wrong'); stub(client, 'exchangeToken').returns(Promise.reject(error)); - const token = await getToken(appCheck as AppCheckService); + const token = await getToken(appCheck as AppCheckService, false, true); expect(reCAPTCHASpy).to.be.called; expect(token).to.deep.equal({ @@ -186,7 +186,7 @@ describe('internal api', () => { const error = new Error('oops, something went wrong'); stub(client, 'exchangeToken').returns(Promise.reject(error)); - const token = await getToken(appCheck as AppCheckService); + const token = await getToken(appCheck as AppCheckService, false, true); expect(token).to.deep.equal({ token: formatDummyToken(defaultTokenErrorData), @@ -208,7 +208,7 @@ describe('internal api', () => { const reCAPTCHASpy = stubGetRecaptchaToken('', false); const exchangeTokenStub = stub(client, 'exchangeToken'); - const token = await getToken(appCheck as AppCheckService); + const token = await getToken(appCheck as AppCheckService, false, true); expect(reCAPTCHASpy).to.be.called; expect(exchangeTokenStub).to.not.be.called; @@ -290,7 +290,6 @@ describe('internal api', () => { }); it('calls 3P error handler if there is an error getting a token', async () => { - stub(console, 'error'); const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true @@ -314,7 +313,6 @@ describe('internal api', () => { }); it('ignores listeners that throw', async () => { - stub(console, 'error'); const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider(FAKE_SITE_KEY), isTokenAutoRefreshEnabled: true diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index 4eb3953614a..eddf043c843 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -60,7 +60,8 @@ export function formatDummyToken( */ export async function getToken( appCheck: AppCheckService, - forceRefresh = false + forceRefresh = false, + shouldLogErrors = false ): Promise { const app = appCheck.app; ensureActivated(app); @@ -136,11 +137,14 @@ export async function getToken( state.token = tokenFromDebugExchange; return { token: tokenFromDebugExchange.token }; } catch (e) { - if ((e as FirebaseError).code === `appCheck/${AppCheckError.THROTTLED}`) { + if ( + (e as FirebaseError).code === `appCheck/${AppCheckError.THROTTLED}` || + (e as FirebaseError).code === + `appCheck/${AppCheckError.INITIAL_THROTTLE}` + ) { // Warn if throttled, but do not treat it as an error. logger.warn((e as FirebaseError).message); - } else { - // `getToken()` should never throw, but logging error text to console will aid debugging. + } else if (shouldLogErrors) { logger.error(e); } // Return dummy token and error @@ -167,11 +171,13 @@ export async function getToken( } token = await getStateReference(app).exchangeTokenPromise; } catch (e) { - if ((e as FirebaseError).code === `appCheck/${AppCheckError.THROTTLED}`) { + if ( + (e as FirebaseError).code === `appCheck/${AppCheckError.THROTTLED}` || + (e as FirebaseError).code === `appCheck/${AppCheckError.INITIAL_THROTTLE}` + ) { // Warn if throttled, but do not treat it as an error. logger.warn((e as FirebaseError).message); - } else { - // `getToken()` should never throw, but logging error text to console will aid debugging. + } else if (shouldLogErrors) { logger.error(e); } // Always save error to be added to dummy token. diff --git a/packages/app-check/src/providers.ts b/packages/app-check/src/providers.ts index 55ab598b5e9..e8d2eb5af5f 100644 --- a/packages/app-check/src/providers.ts +++ b/packages/app-check/src/providers.ts @@ -92,7 +92,7 @@ export class ReCaptchaV3Provider implements AppCheckProvider { Number((e as FirebaseError).customData?.httpStatus), this._throttleData ); - throw ERROR_FACTORY.create(AppCheckError.THROTTLED, { + throw ERROR_FACTORY.create(AppCheckError.INITIAL_THROTTLE, { time: getDurationString( this._throttleData.allowRequestsAfter - Date.now() ), @@ -185,7 +185,7 @@ export class ReCaptchaEnterpriseProvider implements AppCheckProvider { Number((e as FirebaseError).customData?.httpStatus), this._throttleData ); - throw ERROR_FACTORY.create(AppCheckError.THROTTLED, { + throw ERROR_FACTORY.create(AppCheckError.INITIAL_THROTTLE, { time: getDurationString( this._throttleData.allowRequestsAfter - Date.now() ),