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

Aberta
#3,828 0 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 3 dias

@armando-navarro já está trabalhando nisso.

Desde 30/10/2026.

  • #3829 de @armando-navarro — aberto

Avaliação

Dificuldade
4/5
Tempo estimado
3-5 dias
Facilidade para iniciantes
25/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Estagnada
Stack de tecnologia
angular, firebase, typescript
Domínio
frontend

Direção de pesquisa

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.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

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.

Linguagem predominante
TypeScript
Estrelas
7.8k
Forks
2.2k
Merge médio
2d 12h
PRs com merge (30d)
24

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de angular/angularfire

Todas as issues de angular/angularfire

Issues semelhantes

Mais issues de TypeScript

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.