Skip to content

Commit d7f0635

Browse files
committed
Fix onErrors
1 parent 3b0ce02 commit d7f0635

File tree

5 files changed

+119
-81
lines changed

5 files changed

+119
-81
lines changed

packages/app-check-interop-types/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export interface FirebaseAppCheckInternal {
2424
// registered at the same time for one or more FirebaseAppAttestation instances. The
2525
// listeners call back on the UI thread whenever the current token associated with this
2626
// FirebaseAppAttestation changes.
27-
addTokenListener(listener: (token: AppCheckTokenResult) => void): void;
27+
addTokenListener(listener: AppCheckTokenListener): void;
2828

2929
// Unregisters a listener to changes in the token state.
30-
removeTokenListener(listener: (token: AppCheckTokenResult) => void): void;
30+
removeTokenListener(listener: AppCheckTokenListener): void;
3131
}
3232

3333
type AppCheckTokenListener = (token: AppCheckTokenResult) => void;

packages/app-check/src/api.test.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { FirebaseApp } from '@firebase/app-types';
3636
import * as internalApi from './internal-api';
3737
import * as client from './client';
3838
import * as storage from './storage';
39+
import * as logger from './logger';
3940

4041
describe('api', () => {
4142
describe('activate()', () => {
@@ -151,31 +152,32 @@ describe('api', () => {
151152
const errorFn1 = spy();
152153
const errorFn2 = spy();
153154

154-
const unSubscribe1 = onTokenChanged(
155+
const unsubscribe1 = onTokenChanged(
155156
app,
156157
fakePlatformLoggingProvider,
157158
listener1,
158159
errorFn1
159160
);
160-
const unSubscribe2 = onTokenChanged(
161+
const unsubscribe2 = onTokenChanged(
161162
app,
162163
fakePlatformLoggingProvider,
163164
listener2,
164165
errorFn2
165166
);
166167

167-
expect(getState(app).tokenListeners.length).to.equal(2);
168+
expect(getState(app).tokenObservers.length).to.equal(2);
168169

169-
await getToken(app, fakePlatformLoggingProvider);
170+
await internalApi.getToken(app, fakePlatformLoggingProvider);
170171

171172
expect(listener2).to.be.calledWith({
172173
token: fakeRecaptchaAppCheckToken.token
173174
});
174-
expect(errorFn1).to.be.calledOnce;
175+
// onError should not be called on listener errors.
176+
expect(errorFn1).to.not.be.called;
175177
expect(errorFn2).to.not.be.called;
176-
unSubscribe1();
177-
unSubscribe2();
178-
expect(getState(app).tokenListeners.length).to.equal(0);
178+
unsubscribe1();
179+
unsubscribe2();
180+
expect(getState(app).tokenObservers.length).to.equal(0);
179181
});
180182

181183
it('Listeners work when using Observer pattern', async () => {
@@ -206,27 +208,60 @@ describe('api', () => {
206208
* Reverse the order of adding the failed and successful handler, for extra
207209
* testing.
208210
*/
209-
const unSubscribe2 = onTokenChanged(app, fakePlatformLoggingProvider, {
211+
const unsubscribe2 = onTokenChanged(app, fakePlatformLoggingProvider, {
210212
next: listener2,
211213
error: errorFn2
212214
});
213-
const unSubscribe1 = onTokenChanged(app, fakePlatformLoggingProvider, {
215+
const unsubscribe1 = onTokenChanged(app, fakePlatformLoggingProvider, {
214216
next: listener1,
215217
error: errorFn1
216218
});
217219

218-
expect(getState(app).tokenListeners.length).to.equal(2);
220+
expect(getState(app).tokenObservers.length).to.equal(2);
219221

220-
await getToken(app, fakePlatformLoggingProvider);
222+
await internalApi.getToken(app, fakePlatformLoggingProvider);
221223

222224
expect(listener2).to.be.calledWith({
223225
token: fakeRecaptchaAppCheckToken.token
224226
});
225-
expect(errorFn1).to.be.calledOnce;
227+
// onError should not be called on listener errors.
228+
expect(errorFn1).to.not.be.called;
226229
expect(errorFn2).to.not.be.called;
227-
unSubscribe1();
228-
unSubscribe2();
229-
expect(getState(app).tokenListeners.length).to.equal(0);
230+
unsubscribe1();
231+
unsubscribe2();
232+
expect(getState(app).tokenObservers.length).to.equal(0);
233+
});
234+
235+
it('onError() catches token errors', async () => {
236+
stub(logger.logger, 'error');
237+
const app = getFakeApp();
238+
activate(app, FAKE_SITE_KEY, false);
239+
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
240+
const fakeRecaptchaToken = 'fake-recaptcha-token';
241+
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
242+
stub(client, 'exchangeToken').rejects('exchange error');
243+
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));
244+
245+
const listener1 = spy();
246+
247+
const errorFn1 = spy();
248+
249+
const unsubscribe1 = onTokenChanged(
250+
app,
251+
fakePlatformLoggingProvider,
252+
listener1,
253+
errorFn1
254+
);
255+
256+
await internalApi.getToken(app, fakePlatformLoggingProvider);
257+
258+
expect(getState(app).tokenObservers.length).to.equal(1);
259+
260+
expect(errorFn1).to.be.calledOnce;
261+
expect(errorFn1.args[0][0].name).to.include('exchange error');
262+
263+
unsubscribe1();
264+
expect(getState(app).tokenObservers.length).to.equal(0);
230265
});
231266
});
232267
});

packages/app-check/src/internal-api.test.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
defaultTokenErrorData
3636
} from './internal-api';
3737
import * as reCAPTCHA from './recaptcha';
38+
import * as logger from './logger';
3839
import * as client from './client';
3940
import * as storage from './storage';
4041
import { getState, clearState, setState, getDebugState } from './state';
@@ -179,30 +180,21 @@ describe('internal api', () => {
179180
});
180181
});
181182

182-
it('calls optional handler if a listener throws', async () => {
183+
it('calls optional error handler if there is an error getting a token', async () => {
184+
stub(logger.logger, 'error');
183185
activate(app, FAKE_SITE_KEY, true);
184186
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
185-
stub(client, 'exchangeToken').returns(
186-
Promise.resolve(fakeRecaptchaAppCheckToken)
187-
);
188-
const listener1 = (): void => {
189-
throw new Error();
190-
};
191-
const listener2 = spy();
187+
stub(client, 'exchangeToken').rejects('exchange error');
188+
const listener1 = spy();
192189

193190
const errorFn1 = spy();
194-
const errorFn2 = spy();
195191

196192
addTokenListener(app, fakePlatformLoggingProvider, listener1, errorFn1);
197-
addTokenListener(app, fakePlatformLoggingProvider, listener2, errorFn2);
198193

199194
await getToken(app, fakePlatformLoggingProvider);
200195

201-
expect(listener2).to.be.calledWith({
202-
token: fakeRecaptchaAppCheckToken.token
203-
});
204196
expect(errorFn1).to.be.calledOnce;
205-
expect(errorFn2).to.not.be.called;
197+
expect(errorFn1.args[0][0].name).to.include('exchange error');
206198
});
207199

208200
it('loads persisted token to memory and returns it', async () => {
@@ -297,13 +289,13 @@ describe('internal api', () => {
297289

298290
addTokenListener(app, fakePlatformLoggingProvider, listener);
299291

300-
expect(getState(app).tokenListeners[0].listener).to.equal(listener);
292+
expect(getState(app).tokenObservers[0].next).to.equal(listener);
301293
});
302294

303295
it('starts proactively refreshing token after adding the first listener', () => {
304296
const listener = (): void => {};
305297
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
306-
expect(getState(app).tokenListeners.length).to.equal(0);
298+
expect(getState(app).tokenObservers.length).to.equal(0);
307299
expect(getState(app).tokenRefresher).to.equal(undefined);
308300

309301
addTokenListener(app, fakePlatformLoggingProvider, listener);
@@ -391,22 +383,22 @@ describe('internal api', () => {
391383
it('should remove token listeners', () => {
392384
const listener = (): void => {};
393385
addTokenListener(app, fakePlatformLoggingProvider, listener);
394-
expect(getState(app).tokenListeners.length).to.equal(1);
386+
expect(getState(app).tokenObservers.length).to.equal(1);
395387

396388
removeTokenListener(app, listener);
397-
expect(getState(app).tokenListeners.length).to.equal(0);
389+
expect(getState(app).tokenObservers.length).to.equal(0);
398390
});
399391

400392
it('should stop proactively refreshing token after deleting the last listener', () => {
401393
const listener = (): void => {};
402394
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
403395

404396
addTokenListener(app, fakePlatformLoggingProvider, listener);
405-
expect(getState(app).tokenListeners.length).to.equal(1);
397+
expect(getState(app).tokenObservers.length).to.equal(1);
406398
expect(getState(app).tokenRefresher?.isRunning()).to.be.true;
407399

408400
removeTokenListener(app, listener);
409-
expect(getState(app).tokenListeners.length).to.equal(0);
401+
expect(getState(app).tokenObservers.length).to.equal(0);
410402
expect(getState(app).tokenRefresher?.isRunning()).to.be.false;
411403
});
412404
});

packages/app-check/src/internal-api.ts

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '@firebase/app-check-interop-types';
2424
import {
2525
AppCheckTokenInternal,
26-
AppCheckTokenListenerInternal,
26+
AppCheckTokenObserver,
2727
getDebugState,
2828
getState,
2929
setState
@@ -172,13 +172,13 @@ export function addTokenListener(
172172
onError?: (error: Error) => void
173173
): void {
174174
const state = getState(app);
175-
const tokenListener: AppCheckTokenListenerInternal = {
176-
listener,
177-
onError
175+
const tokenListener: AppCheckTokenObserver = {
176+
next: listener,
177+
error: onError
178178
};
179179
const newState = {
180180
...state,
181-
tokenListeners: [...state.tokenListeners, tokenListener]
181+
tokenObservers: [...state.tokenObservers, tokenListener]
182182
};
183183

184184
/**
@@ -191,14 +191,8 @@ export function addTokenListener(
191191
if (debugState.enabled && debugState.token) {
192192
debugState.token.promise
193193
.then(token => listener({ token }))
194-
.catch(e => {
195-
/**
196-
* An error handler will be provided if this is called by the public
197-
* API. Internal callers don't care about errors in listeners.
198-
*/
199-
if (onError) {
200-
onError(e);
201-
}
194+
.catch(() => {
195+
/** Ignore errors in listeners. */
202196
});
203197
}
204198
} else {
@@ -208,7 +202,11 @@ export function addTokenListener(
208202
* invoke the listener with the valid token, then start the token refresher
209203
*/
210204
if (!newState.tokenRefresher) {
211-
const tokenRefresher = createTokenRefresher(app, platformLoggerProvider);
205+
const tokenRefresher = createTokenRefresher(
206+
app,
207+
platformLoggerProvider,
208+
onError
209+
);
212210
newState.tokenRefresher = tokenRefresher;
213211
}
214212

@@ -226,14 +224,8 @@ export function addTokenListener(
226224
const validToken = state.token;
227225
Promise.resolve()
228226
.then(() => listener({ token: validToken.token }))
229-
.catch(e => {
230-
/**
231-
* An error handler will be provided if this is called by the public
232-
* API. Internal callers don't care about errors in listeners.
233-
*/
234-
if (onError) {
235-
onError(e);
236-
}
227+
.catch(() => {
228+
/** Ignore errors in listeners. */
237229
});
238230
}
239231
}
@@ -247,11 +239,11 @@ export function removeTokenListener(
247239
): void {
248240
const state = getState(app);
249241

250-
const newListeners = state.tokenListeners.filter(
251-
tokenListener => tokenListener.listener !== listener
242+
const newObservers = state.tokenObservers.filter(
243+
tokenObserver => tokenObserver.next !== listener
252244
);
253245
if (
254-
newListeners.length === 0 &&
246+
newObservers.length === 0 &&
255247
state.tokenRefresher &&
256248
state.tokenRefresher.isRunning()
257249
) {
@@ -260,13 +252,14 @@ export function removeTokenListener(
260252

261253
setState(app, {
262254
...state,
263-
tokenListeners: newListeners
255+
tokenObservers: newObservers
264256
});
265257
}
266258

267259
function createTokenRefresher(
268260
app: FirebaseApp,
269-
platformLoggerProvider: Provider<'platform-logger'>
261+
platformLoggerProvider: Provider<'platform-logger'>,
262+
onError?: (error: Error) => void
270263
): Refresher {
271264
return new Refresher(
272265
// Keep in mind when this fails for any reason other than the ones
@@ -284,7 +277,11 @@ function createTokenRefresher(
284277

285278
// getToken() always resolves. In case the result has an error field defined, it means the operation failed, and we should retry.
286279
if (result.error) {
287-
throw result.error;
280+
if (onError) {
281+
onError(result.error);
282+
} else {
283+
throw result.error;
284+
}
288285
}
289286
},
290287
() => {
@@ -322,17 +319,24 @@ function notifyTokenListeners(
322319
app: FirebaseApp,
323320
token: AppCheckTokenResult
324321
): void {
325-
const listeners = getState(app).tokenListeners;
322+
const observers = getState(app).tokenObservers;
326323

327-
for (const listener of listeners) {
324+
for (const observer of observers) {
328325
try {
329-
listener.listener(token);
330-
} catch (e) {
331-
// If any listener fails, run any provided error handler,
332-
// then run next listener.
333-
if (listener.onError) {
334-
listener.onError(e);
326+
if (observer.error) {
327+
// If this listener has an error handler, handle errors differently
328+
// from successes.
329+
if (token.error) {
330+
observer.error(token.error);
331+
} else {
332+
observer.next(token);
333+
}
334+
} else {
335+
// Otherwise return the token, whether or not it has an error field.
336+
observer.next(token);
335337
}
338+
} catch (ignored) {
339+
// If any handler fails, ignore and run next handler.
336340
}
337341
}
338342
}

0 commit comments

Comments
 (0)