Repository navigation
feat(app): add initializeBrowserOrServerApp for browser & server apps - #3844
armando-navarro wants to merge 2 commits into
Conversation
One call inside provideFirebaseApp that returns the ordinary app in the browser and, on the server, a FirebaseServerApp for the visitor's ID token and App Check token, read from REQUEST_CONTEXT or passed in the settings. With neither token it returns the ordinary app. The server app is released when the render is garbage collected, as the releaseOnDeref setup in the guide does. releaseAfterRender opts into a release at render end or a fixed time after it. A token Firebase cannot parse now renders signed out with one warning instead of an HTTP 500. In dev mode a request with no request context is noted once. The App Check server warning now says which renders send no token. The Node specs can import the built library through a resolve hook. Fixes angular#3843
tyler-reitz
left a comment
There was a problem hiding this comment.
The function itself looks solid. I checked refcount balance on every path, cross-visitor isolation (the token check at :173 correctly catches an SDK name-hash collision, which I reproduced with two JWT-shaped tokens), and that no token text reaches a log line. The fallback catch is scoped to the SDK call only.
Two things before approving, both small:
- The "default release is not affected" claim doesn't hold, and the timed-mode caveat is narrower than the real behavior. Inline at
:151and:183. The same sentences are in #3835'sdocs/auth.md:246, so it'd be good to fix both together. - Three spec gaps on the release path. Mutants that break it still pass the suite. Inline in the spec file.
Fine as follow-up issues after rc.1, not blocking:
- Make the release survive a teardown throw (e.g. register it before anything else can throw, or catch around it).
- A
provideFirebaseAppin a lazy route's providers never releases, since route injectors aren't destroyed at app teardown. - The resolve hook reads only the
defaultexport condition, so a futurenodecondition would be hidden from the Node specs. - Only the
authIdTokenhalf of the mismatch check at:173is tested.
| const { releaseOnDeref: _releaseOnDeref, releaseAfterRender, automaticDataCollectionEnabled: _automaticDataCollectionEnabled, ...serverSettings } = settings; | ||
| const timedRelease = releaseAfterRender !== undefined || typeof FinalizationRegistry === 'undefined'; | ||
|
|
||
| // Kept reachable only through the injector's destroy hooks, which Angular empties at destroy, so the SDK can collect it right after the render. |
There was a problem hiding this comment.
The default mode can leak too. If a teardown hook throws (e.g. a service's ngOnDestroy), R3Injector.destroy exits before it clears _onDestroyHooks, so the injector keeps () => renderHolder. Then if the render also left a zone-wrapped listener on the server app (say a root service calling onAuthStateChanged(auth, ...) without unsubscribing), the server app keeps that injector reachable through the listener closure. The holder is never collected and the app never releases. Reproduced with --expose-gc: a throw on its own frees the app, and only a throw plus a listener leaks it.
So this comment and "The default release does not depend on teardown and is not affected" (#3835, docs/auth.md:246, and the PR body) need rewording, e.g. "is affected only when the render also leaves a listener on the server app".
|
|
||
| if (timedRelease) { | ||
| // Inside an environment injector DestroyRef is the injector itself, so this runs when the render's injector is destroyed. | ||
| destroyRef.onDestroy(() => scheduleRelease(serverApp, releaseAfterRender ?? 0)); |
There was a problem hiding this comment.
Any teardown throw skips this, not just a service's ngOnDestroy. I ran it with an ApplicationRef.onDestroy listener that throws: the server app stays in _serverApps. From reading Angular 22's teardown (not run), a component ngOnDestroy throw escapes the same way, through ApplicationRef.ngOnDestroy. That's a common SSR bug, e.g. this.sub.unsubscribe() on a browser-only subscription. Worth saying "any ngOnDestroy or destroy hook" in the caveat.
| }); | ||
|
|
||
| describe('releasing the server app', () => { | ||
| it('by default hands the SDK its own object to release the app at garbage collection', async () => { |
There was a problem hiding this comment.
Nothing here checks that the default-mode release actually happens after GC. Two mutants pass the full suite:
- Delete the keep-alive at
:154. The server app can then be deleted mid-render. - Push
renderHolderinto a module-level array. That leaks one app per visitor.
The callThrough spy also records the holder as a call argument, which keeps it reachable. A spec that drops the spy and forces a collection (--expose-gc) catches both. Have the spec assert the app is still alive while the render runs, then deleted once it's gone.
| } | ||
| }); | ||
|
|
||
| it('shares one app between two renders with the same token and releases it after the last', async () => { |
There was a problem hiding this comment.
This catches a double release at delay 0 but not with a positive releaseAfterRender: making the timer at :130 call release twice still passes. Same spec with e.g. releaseAfterRender: 100, asserting the app survives the first timer?
| }); | ||
| }); | ||
|
|
||
| describe('tokens the SDK rejects', () => { |
There was a problem hiding this comment.
The TypeError half of isTokenParseError has no spec. If the check is narrowed to SyntaxError only, the suite stays green, and a token whose payload is null (h.${Buffer.from('null').toString('base64url')}.s) then fails the render with a 500.
Fixes #3843
docs/auth.md,docs/app-check.mdanddocs/version-22-upgrade.mdchanges go on the rc.1 docs pull request, docs: document the 22.0.0-rc.1 changes #3835, which merges just before the tag.src/app/browser-or-server-app.jasmine.ts.src/app/app.spec.tsand two insrc/app-check/app-check.spec.ts.yarn install,yarn testrun successfully? yes, with npm:npm ci,npm run build,npm run lint,npm run test:nodeon Node 22, 24 and 26, andnpm run test:chrome-headless.Description
This adds
initializeBrowserOrServerAppto@angular/fire/app, one call insideprovideFirebaseAppthat returns the ordinary app in the browser and aFirebaseServerAppfor the visitor's tokens on the server. It replaces the callback the Authentication guide asks server-rendered apps to write. The reasoning is in the linked issue.Changes
src/app/browser-or-server-app.ts, exported from@angular/fire/app:initializeBrowserOrServerApp(options, settings?). On the server it readsauthIdTokenandappCheckTokenfromREQUEST_CONTEXT, or fromsettingswhen passed there, and builds a server app when either is present.releaseOnDerefworks, so a repeat request from the same visitor reuses it.settings.releaseAfterRenderopts into a release at render end (0) or that many milliseconds after it.FinalizationRegistrythe release is at render end.releaseOnDerefis typednever, and stripped at runtime, so one server app is never released twice.SyntaxErrororTypeError) returns the ordinary app after one fixed warning per process, then one line per hundred. EveryFirebaseErrorstill propagates.src/app-check/app-check.module.ts: the dev-mode server warning now says that server renders without a relayed App Check token send none, and points at the new function.src/app/app.ts: the comment's usage example no longer showsdeleteAppon the injected app.tools/angular-fire-resolve-hook.mjsandtools/jasmine-register-angular-fire.mjs, preloaded bytest:nodeandtest:node-esm: a resolve hook that maps@angular/fireimports to the built library, so Node specs can import it. No Node spec could before.Behavior to know
provideFirebaseAppand passinject(FirebaseApp)to every product provider, as the guide says.deleteAppon the injected app when using the function. It releases the server app itself.releaseAfterRender: 0every repeat request pays that cost again, which the docs state.releaseAfterRenderset, a service whosengOnDestroythrows stops Angular's teardown before the release runs, and that render's server app stays behind. The default release is not affected.Verified outside CI
On a fresh Angular 22 app wired by following the rewritten guide literally, against a real project:
releaseAfterRender: 0, after about a second with1000). Also underng serveand in an NgModule-bootstrapped app.app.config.client.tsandapp.config.server.tslayout renders signed out and signed in. The production browser bundle is the same size with and without the function.firebase/firestoreread still pending when the app is released at render end resolves normally, and a client abort mid-render lets the render finish and release its app.Tyler, the part I'd most like a second look at is the release in
buildServerApp: the default hands the SDK a holder object kept alive only by the render's injector, and the timed mode releases from the injector'sonDestroy.