Angular SSR v16: saying goodbye to a sneaky trick - macrotask wrapping for HTTP calls 👋
Tags: angular, ssr, angularuniversal, zonejs
Published: 2023-07-03 06:41:11

**Since version 16, Angular SSR no longer counts outgoing HTTP requests as macrotasks in `Zone.js`, but uses an internal http interceptor function instead. As a consequence, if you used a technique of [tracking `NgZone` macrotasks for debugging hanging SSR](https://dev.to/krisplatis/how-to-find-out-why-angular-ssr-hangs-track-ngzone-tasks-4nbd), it will no longer reveal long pending HTTP requests for you. But the good news is you can still implement yourself a custom [`HttpInterceptor`](https://angular.io/api/common/http/HttpInterceptor) for counting pending requests.**

### Before Angular 16 - wrapping HTTP requests as macrotasks
Before version 16, Angular created in `Zone.js` an artificial macrotask named [`ZoneMacroTaskWrapper.subscribe`](https://github.com/angular/angular/blob/15.2.x/packages/platform-server/src/http.ts#L85-L86) for [each outgoing HTTP request](https://github.com/angular/angular/blob/15.2.x/packages/platform-server/src/http.ts#L121) in the [special HttpHandler](https://github.com/angular/angular/blob/15.2.x/packages/platform-server/src/http.ts#L105-L106). Angular completed manually such an artifical macrotask [when the HTTP request completed](https://github.com/angular/angular/blob/15.2.x/packages/platform-server/src/http.ts#L58). Thanks to this trick, `Zone.js` treated every pending outgoing HTTP request as a macrotask, even though by nature HTTP requests are not JavaScript macrotasks.

The observable [`ApplicationRef.isStable`](https://angular.io/api/core/ApplicationRef#isStable), which is used internally by Angular SSR [to hold the serialization of HTML until the app is stable](https://github.com/angular/angular/blob/16.0.x/packages/platform-server/src/utils.ts#L59), depended directly on the [`NgZone.onStable`](https://github.com/angular/angular/blob/15.2.x/packages/core/src/application_ref.ts#L799-L804), which emitted only when there were no pending microtasks and macrotasks (so also when there were no pending outgoing HTTP requests).

## Since Angular 16 - no more wrapping HTTP requests as macrotasks
Since version 16, Angular removed the trick of wrapping HTTP calls as `Zone.js` macrotasks. And Angular came up with a new, complementary solution for waiting until HTTP requests complete. Now Angular tracks pending HTTP requests with a special new class [`InitialRenderPendingTasks`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/initial_render_pending_tasks.ts#L24) which counts every pending http call via a new Angular's [internal http interceptor](https://github.com/angular/angular/blob/16.1.x/packages/common/http/src/interceptor.ts#L182-L184) function. 
Now `ApplicationRef.isStable` takes into account [not only the observable `NgZone.onStable`, but also `InitialRenderPendingTasks.hasPendingTasks`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/application_ref.ts#L848-L850).

So the old behavior is preserved - SSR is waiting for outgoing HTTP requests to complete. But it's just achieved differently now, with the help of the new class `InitialRenderPendingTasks`.

Side note: the new class [`InitialRenderPendingTasks` is used also by Angular Router](https://github.com/angular/angular/blob/16.0.x/packages/router/src/router.ts#L181) to [ensure waiting for the initial navigation to complete](https://github.com/angular/angular/blob/16.0.x/packages/router/src/router.ts#L732-L737).

## How can I track outgoing requests in SSR?
You can write a custom Angular [`HTTP_INTERCEPTOR`](https://angular.io/api/common/http/HttpInterceptor), which will track pending outgoing requests. The following is an example implementation of such a custom interceptor:

```ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

interface RequestDetails {
  url: string;
  requestTime: Date;
  responseTime: Date | undefined;
}

@Injectable()
export class DebuggingInterceptor implements HttpInterceptor {
  private requests: RequestDetails[] = [];

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.requests.push({ 
      url: request.url,
      requestTime: new Date()
      responseTime: undefined
    });

    return next.handle(request).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          requestDetails.responseTime = new Date();
        }
      }, (error) => {
          requestDetails.responseTime = new Date();
      })
    );
  }
}
```

Note: the above implementation is just an example and can be enhanced by adding more request's details, for example the response HTTP status.
Note 2: Such an interceptor can be also implemented when using Angular versions prior to 16. 


## ZoneJS still triggers change detection on HTTP request completion
Even though HTTP requests are no longer wrapped as artifical macrotasks in Angular's internals, the change detection in Angular app still will be triggered after an outgoing request completes. It's possioble, because `Zone.js` tracks also native event listeners (including native event listeners responsible for outgoing http request) and then triggers [`NgZone.onLeave()`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/zone/ng_zone.ts#L418), which triggers an internal function [`checkStable()`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/zone/ng_zone.ts#L480), which triggers emission of  [`NgZone.onMicrotaskEmpty`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/zone/ng_zone.ts#L345) (btw. I wonder why it wasn't named `NgZone.onTaskEmpty`, because it's not only about microtasks, but also event listeners), which triggers emission of  [`ApplicationRef.tick()`](https://github.com/angular/angular/blob/16.0.x/packages/core/src/application_ref.ts#L1231-L1234), which performs the [change detection](https://github.com/angular/angular/blob/16.0.x/packages/core/src/application_ref.ts#L1052).