`ɵzoneWrap` ignores a handler passed as an observer object, and holds a wrapped promise forever once its injector is destroyed

Ouverte
#3,828 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 3 jours

@armando-navarro y travaille déjà.

Depuis le 30/10/2026.

  • #3829 par @armando-navarro — ouverte

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
25/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
À l'abandon
Stack technique
angular, firebase, typescript
Domaine
frontend

Piste de recherche

Start by reading src/zones.ts, where ɵzoneWrap handles callbacks and promises. Reproduce the observer-object and destroyed-injector cases described in the issue, then check that object handlers receive the same task, injection-context and zone behavior as function handlers, and that wrapped promises settle after injector destruction. A linked pull request is already open.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

comp: app-check comp: core comp: firestore comp: remote-config comp: zones type: bug version: current (v17+)
Version info

Angular: 22.2

Firebase: 12.19.0

AngularFire: 22.0.0-rc.0, and main

Description

Two defects in ɵzoneWrap (src/zones.ts), the function the generated @angular/fire/* wrappers go through.

1. A handler passed as an object is not wrapped. Firebase accepts a subscription handler either as a function, onSnapshot(ref, snapshot => ...), or as an observer object, onSnapshot(ref, { next: snapshot => ... }). The wrapper rewrites an argument only when typeof is 'function', so the object form reaches the SDK untouched:

  • No pending task is registered, so server rendering does not wait for the first value.
  • inject() throws inside the handler, because nothing re-enters the injection context.
  • In a zone.js app the handler runs outside the Angular zone, because the wrapper calls Firebase inside runOutsideAngular and then never brings the object's members back in. The unwrapped Firebase call made from in-zone code keeps its handler in the zone, so the wrapper makes the object form worse than not wrapping at all.

Eleven wrapped functions accept the object form: onAuthStateChanged, onIdTokenChanged, onTokenChanged, onSnapshot, onSnapshotResume, onSnapshotsInSync, onMessage, onRegistered, onUnregistered, onConfigUpdate (which accepts only the object form) and data-connect's subscribe (whose object uses onNext, onErr and onComplete). The three messaging functions are generated with blockUntilFirst: false, so the pending task point does not apply to them.

2. A wrapped promise never settles after its injector is destroyed.

  • When a wrapped call returns a Promise, the wrapper resolves its own promise through runInInjectionContext(injector, () => run(() => resolve(value))).
  • Neither layer reaches the caller: a .then callback runs as a later microtask after the injection context is torn down, and zone.js runs it in the zone where .then was attached, not where resolve was called.
  • The one thing the layers do is throw: runInInjectionContext checks assertNotDestroyed, so when the Firebase promise settles after ApplicationRef.destroy() or a TestBed reset, the callback throws NG0205 before resolve runs and the caller's promise never settles.
How to reproduce these conditions

On a zone.js Angular 22.2 app and a zoneless one, against the Firestore emulator, call the real functions through ɵzoneWrap the way a constructor does, inside the Angular zone and an injection context, and record inside the handler whether a pending task was registered, whether inject() works and whether NgZone.isInAngularZone() is true:

  • onSnapshot(ref, { next, error }) and onSnapshotsInSync(firestore, { next, error }): no task in either app, inject() throws in both, and in the zone.js app the handler runs outside the Angular zone. A plain property written from the handler does not render there, in a component with default change detection.
  • onSnapshot(ref, next), the function form: one task, released when the handler ran, inject() works, and in the zone.js app the handler runs inside the zone.
  • The unwrapped onSnapshot with either form of handler, called from the same place: inject() throws, but in the zone.js app the handler stays inside the zone.

For the promise defect, a spec: wrap a function that resolves after 20 ms with ɵzoneWrap(fn, true), call it inside TestBed.runInInjectionContext, call TestBed.resetTestingModule(), then await the returned promise. It never settles and the spec times out after 5 seconds.

Expected behavior

A handler passed as an observer object gets what a handler passed as a function gets: the pending task, the Angular zone and a working inject(). A wrapped promise settles whenever the Firebase promise settles.

Actual behavior

The object form gets none of the three, and a wrapped promise that settles after its injector is destroyed never settles.

Langage dominant
TypeScript
Étoiles
7.8k
Forks
2.2k
Merge moyen
2 j 12 h
PR mergées (30 j)
24

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de angular/angularfire

Toutes les issues de angular/angularfire

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.