Skip to content

Commit 5795e03

Browse files
committed
fix(http): Delay stabilization until next app synchronization (#60656)
This commit updates the `HttpClient` internals to use the public `PendingTasks` API which delays stability until the next `ApplicationRef.tick` instead of causing the application to become stable synchronously. This is helpful to resolve unexpected issues where computations happen as follow-up work to the value coming out of the response. fixes #59352 PR Close #60656
1 parent 2348f26 commit 5795e03

File tree

2 files changed

+28
-7
lines changed

2 files changed

+28
-7
lines changed

packages/common/http/src/interceptor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
runInInjectionContext,
1717
ɵConsole as Console,
1818
ɵformatRuntimeError as formatRuntimeError,
19-
ɵPendingTasksInternal as PendingTasks,
19+
PendingTasks,
2020
} from '@angular/core';
2121
import {Observable} from 'rxjs';
2222
import {finalize} from 'rxjs/operators';
@@ -244,8 +244,8 @@ export function legacyInterceptorFnFactory(): HttpInterceptorFn {
244244
const pendingTasks = inject(PendingTasks);
245245
const contributeToStability = inject(REQUESTS_CONTRIBUTE_TO_STABILITY);
246246
if (contributeToStability) {
247-
const taskId = pendingTasks.add();
248-
return chain(req, handler).pipe(finalize(() => pendingTasks.remove(taskId)));
247+
const removeTask = pendingTasks.add();
248+
return chain(req, handler).pipe(finalize(removeTask));
249249
} else {
250250
return chain(req, handler);
251251
}
@@ -323,10 +323,10 @@ export class HttpInterceptorHandler extends HttpHandler {
323323
}
324324

325325
if (this.contributeToStability) {
326-
const taskId = this.pendingTasks.add();
326+
const removeTask = this.pendingTasks.add();
327327
return this.chain(initialRequest, (downstreamRequest) =>
328328
this.backend.handle(downstreamRequest),
329-
).pipe(finalize(() => this.pendingTasks.remove(taskId)));
329+
).pipe(finalize(removeTask));
330330
} else {
331331
return this.chain(initialRequest, (downstreamRequest) =>
332332
this.backend.handle(downstreamRequest),

packages/common/http/test/provider_spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ApplicationRef,
2727
createEnvironmentInjector,
2828
EnvironmentInjector,
29+
ErrorHandler,
2930
inject,
3031
InjectionToken,
3132
PLATFORM_ID,
@@ -57,15 +58,35 @@ describe('without provideHttpClientTesting', () => {
5758
});
5859

5960
let stable = false;
61+
const appRef = TestBed.inject(ApplicationRef);
6062
TestBed.inject(ApplicationRef).isStable.subscribe((v) => {
6163
stable = v;
6264
});
6365

6466
expect(stable).toBe(true);
6567
TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
6668
expect(stable).toBe(false);
67-
await Promise.resolve();
68-
expect(stable).toBe(true);
69+
await expectAsync(appRef.whenStable()).toBeResolved();
70+
});
71+
72+
it('can use an ErrorHandler that injects HttpClient', () => {
73+
// This test ensures there is no circular dependency:
74+
// possible loops are:
75+
// INTERNAL_APPLICATION_ERROR_HANDLER -> ErrorHandler -> HttpClient -> PendingTasks -> ChangeDetectionScheduler -> INTERNAL_APPLICATION_ERROR_HANDLER
76+
TestBed.configureTestingModule({
77+
providers: [
78+
provideHttpClient(withInterceptors([() => from(Promise.resolve(new HttpResponse()))])),
79+
{
80+
provide: ErrorHandler,
81+
useClass: class extends ErrorHandler {
82+
client = inject(HttpClient);
83+
override handleError(error: any): void {}
84+
},
85+
},
86+
],
87+
});
88+
89+
expect(() => TestBed.inject(ApplicationRef)).not.toThrow();
6990
});
7091
});
7192

0 commit comments

Comments
 (0)