diff --git a/.changeset/red-actors-care.md b/.changeset/red-actors-care.md new file mode 100644 index 00000000000..2a3898ba647 --- /dev/null +++ b/.changeset/red-actors-care.md @@ -0,0 +1,5 @@ +--- +"@firebase/auth": patch +--- + +Fix errors during Auth initialization when the network is unavailable diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index e34a845669e..0579a088d14 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -135,7 +135,10 @@ export class AuthImpl implements AuthInternal, _FirebaseService { // Initialize the resolver early if necessary (only applicable to web: // this will cause the iframe to load immediately in certain cases) if (this._popupRedirectResolver?._shouldInitProactively) { - await this._popupRedirectResolver._initialize(this); + // If this fails, don't halt auth loading + try { + await this._popupRedirectResolver._initialize(this); + } catch (e) { /* Ignore the error */ } } await this.initializeCurrentUser(popupRedirectResolver); diff --git a/packages/auth/src/platform_browser/auth.test.ts b/packages/auth/src/platform_browser/auth.test.ts index eecfc4c29f2..fd4aac82df8 100644 --- a/packages/auth/src/platform_browser/auth.test.ts +++ b/packages/auth/src/platform_browser/auth.test.ts @@ -216,6 +216,16 @@ describe('core/auth/initializeAuth', () => { expect(resolverInternal._initialize).to.have.been.called; }); + it('does not halt init if resolver fails', async () => { + const popupRedirectResolver = makeMockPopupRedirectResolver(); + const resolverInternal: PopupRedirectResolverInternal = _getInstance( + popupRedirectResolver + ); + sinon.stub(resolverInternal, '_shouldInitProactively').value(true); + sinon.stub(resolverInternal, '_initialize').rejects(new Error()); + await expect(initAndWait(inMemoryPersistence, popupRedirectResolver)).not.to.be.rejected; + }); + it('reloads non-redirect users', async () => { sinon .stub(_getInstance(inMemoryPersistence), '_get') diff --git a/packages/auth/src/platform_browser/iframe/gapi.test.ts b/packages/auth/src/platform_browser/iframe/gapi.test.ts index f0cfefba7e2..c1a4ef3b080 100644 --- a/packages/auth/src/platform_browser/iframe/gapi.test.ts +++ b/packages/auth/src/platform_browser/iframe/gapi.test.ts @@ -33,13 +33,14 @@ use(chaiAsPromised); describe('platform_browser/iframe/gapi', () => { let library: typeof gapi; let auth: TestAuth; + let loadJsStub: sinon.SinonStub; function onJsLoad(globalLoadFnName: string): void { _window().gapi = library as typeof gapi; _window()[globalLoadFnName](); } beforeEach(async () => { - sinon.stub(js, '_loadJS').callsFake(url => { + loadJsStub = sinon.stub(js, '_loadJS').callsFake(url => { onJsLoad(url.split('onload=')[1]); return Promise.resolve(new Event('load')); }); @@ -134,4 +135,10 @@ describe('platform_browser/iframe/gapi', () => { ); expect(_loadGapi(auth)).not.to.eq(firstAttempt); }); + + it('rejects if gapi itself does not load', async () => { + const error = new Error(); + loadJsStub.rejects(error); + await expect(_loadGapi(auth)).to.be.rejectedWith(error); + }); }); diff --git a/packages/auth/src/platform_browser/iframe/gapi.ts b/packages/auth/src/platform_browser/iframe/gapi.ts index cc637546544..b3349ad02ac 100644 --- a/packages/auth/src/platform_browser/iframe/gapi.ts +++ b/packages/auth/src/platform_browser/iframe/gapi.ts @@ -103,7 +103,7 @@ function loadGapi(auth: AuthInternal): Promise { } }; // Load GApi loader. - return js._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`); + return js._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`).catch(e => reject(e)); } }).catch(error => { // Reset cached promise to allow for retrial. diff --git a/packages/auth/src/platform_browser/popup_redirect.test.ts b/packages/auth/src/platform_browser/popup_redirect.test.ts index 05c3b871a95..9d6924d4ae0 100644 --- a/packages/auth/src/platform_browser/popup_redirect.test.ts +++ b/packages/auth/src/platform_browser/popup_redirect.test.ts @@ -54,6 +54,7 @@ describe('platform_browser/popup_redirect', () => { let auth: TestAuth; let onIframeMessage: (event: GapiAuthEvent) => Promise; let iframeSendStub: sinon.SinonStub; + let loadGapiStub: sinon.SinonStub; beforeEach(async () => { auth = await testAuth(); @@ -61,8 +62,18 @@ describe('platform_browser/popup_redirect', () => { sinon.stub(validateOrigin, '_validateOrigin').returns(Promise.resolve()); iframeSendStub = sinon.stub(); + loadGapiStub = sinon.stub(gapiLoader, '_loadGapi'); + setGapiStub(); - sinon.stub(gapiLoader, '_loadGapi').returns( + sinon.stub(authWindow._window(), 'gapi').value({ + iframes: { + CROSS_ORIGIN_IFRAMES_FILTER: 'cross-origin-iframes-filter' + } + }); + }); + + function setGapiStub(): void { + loadGapiStub.returns( Promise.resolve(({ open: () => Promise.resolve({ @@ -74,13 +85,7 @@ describe('platform_browser/popup_redirect', () => { }) } as unknown) as gapi.iframes.Context) ); - - sinon.stub(authWindow._window(), 'gapi').value({ - iframes: { - CROSS_ORIGIN_IFRAMES_FILTER: 'cross-origin-iframes-filter' - } - }); - }); + } afterEach(() => { sinon.restore(); @@ -241,6 +246,14 @@ describe('platform_browser/popup_redirect', () => { expect(resolver._initialize(secondAuth)).to.eq(secondPromise); }); + it('clears the cache if the initialize fails', async () => { + const error = new Error(); + loadGapiStub.rejects(error); + await expect(resolver._initialize(auth)).to.be.rejectedWith(error); + setGapiStub(); // Reset the gapi load stub + await expect(resolver._initialize(auth)).not.to.be.rejected; + }); + it('iframe event goes through to the manager', async () => { const manager = (await resolver._initialize(auth)) as AuthEventManager; sinon.stub(manager, 'onEvent').returns(true); diff --git a/packages/auth/src/platform_browser/popup_redirect.ts b/packages/auth/src/platform_browser/popup_redirect.ts index 26a4deb2ec9..aa95d0e3d0a 100644 --- a/packages/auth/src/platform_browser/popup_redirect.ts +++ b/packages/auth/src/platform_browser/popup_redirect.ts @@ -111,6 +111,13 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolverInternal { const promise = this.initAndGetManager(auth); this.eventManagers[key] = { promise }; + + // If the promise is rejected, the key should be removed so that the + // operation can be retried later. + promise.catch(() => { + delete this.eventManagers[key]; + }); + return promise; }