DEV Community

Krzysztof Platis
Krzysztof Platis

Posted on • Edited on

Angular SSR v16: saying goodbye to a sneaky trick - macrotask wrapping for HTTP calls 👋

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, it will no longer reveal long pending HTTP requests for you. But the good news is you can still implement yourself a custom 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 for each outgoing HTTP request in the special HttpHandler. Angular completed manually such an artifical macrotask when the HTTP request completed. 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, which is used internally by Angular SSR to hold the serialization of HTML until the app is stable, depended directly on the NgZone.onStable, 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 which counts every pending http call via a new Angular's internal http interceptor function.
Now ApplicationRef.isStable takes into account not only the observable NgZone.onStable, but also InitialRenderPendingTasks.hasPendingTasks.

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 to ensure waiting for the initial navigation to complete.

How can I track outgoing requests in SSR?

You can write a custom Angular HTTP_INTERCEPTOR, which will track pending outgoing requests. The following is an example implementation of such a custom interceptor:

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();
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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(), which triggers an internal function checkStable(), which triggers emission of NgZone.onMicrotaskEmpty (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(), which performs the change detection.

If you really feel like buying me a coffee

... then feel free to do it. Many thanks! 🙌

Buy Me A Coffee

Top comments (1)

Collapse
 
tilesto profile image
Tigran Petrosyan • Edited

Thanks a lot for the article! And other articles too, they are really useful (at least for me and my stack: Angular 16, Universal).

It's off-top, but a question: how to update meta tags in Universal?
via { Meta, Title } from '@angular/platform-browser' - not working. It updates in dev-tools, but not in the view-source. Do you have some article about it?)
It looks like an issue: github.com/angular/angular/issues/...
Because it was working for me for Angular 14-15, but for 16 - it doesn't update meta info