Skip to the solution
MediumFrontend9 min read

How do functional interceptors work with provideHttpClient()?

988 views

The problem

Question presented to candidate: "Say we register three functional interceptors with provideHttpClient(). Walk me through exactly what happens when a component calls http.get(): which code runs in which order on the way out and on the way back, and what changes if a lazy route registers its own provideHttpClient()."

What a strong answer should cover:

  • provideHttpClient() wires HttpClient to an internal HttpInterceptorHandler, which folds the registered functions into one chain with reduceRight, ending at the HttpBackend.
  • Request phase runs in array order; response phase runs in reverse, because each interceptor wraps everything after it (an onion).
  • Code before next() is the request phase; operators piped onto next() are the response phase.
  • Short-circuiting: not calling next() skips everything downstream (caches, mocks); per-request flags travel in HttpContext.
  • A built-in XSRF interceptor is always first; the chain is built lazily on the first request and each interceptor runs in an injection context.
  • A child provideHttpClient() creates an independent chain; withRequestsMadeViaParent() makes it forward to the parent.

Clarifying questions expected:

  • "Do you want the order of side effects, or also how errors propagate back up?"
  • "Are there route-level HttpClient providers in this app?"

Code / implementation expected: Yes β€” instrumented interceptors that log in/out order, plus a short-circuiting cache keyed by an HttpContextToken.

  • http
  • interceptors
  • standalone

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

AngularInstrumented interceptor chain with a short-circuiting HttpContext cache
import { ApplicationConfig, Component, inject, signal } from "@angular/core";
import {
  HttpClient, HttpContext, HttpContextToken, HttpEventType, HttpInterceptorFn, HttpResponse,
  provideHttpClient, withInterceptors,
} from "@angular/common/http";
import { of, tap } from "rxjs";

export const trace: string[] = [];

// Per-request flag an interceptor can read. Default: caching off.
export const CACHE = new HttpContextToken<boolean>(() => false);

// Code before next() runs on the way in; operators piped after next() run on the way out.
const stage = (name: string): HttpInterceptorFn => (req, next) => {
  trace.push(`${name} in`);
  return next(req).pipe(
    tap((e) => { if (e.type === HttpEventType.Response) trace.push(`${name} out`); }),
  );
};

// Short-circuit: answer from a cache and never call next(), so later stages never run.
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  if (!req.context.get(CACHE)) return next(req);
  const hit = cache.get(req.urlWithParams);
  if (hit) { trace.push("cache HIT"); return of(hit.clone()); }
  return next(req).pipe(tap((e) => { if (e instanceof HttpResponse) cache.set(req.urlWithParams, e); }));
};

let calls = 0;
const fakeBackend: HttpInterceptorFn = (req) => {
  trace.push("backend");
  return of(new HttpResponse({ status: 200, body: { n: ++calls } }));
};

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([stage("A"), cacheInterceptor, stage("B"), fakeBackend])),
  ],
};

@Component({
  selector: "app-root",
  template: `
    <button id="plain" (click)="get(false)">Plain GET</button>
    <button id="cached" (click)="get(true)">Cached GET</button>
    <p>{{ log() }}</p>
  `,
})
export class AppComponent {
  private http = inject(HttpClient);
  log = signal("");

  get(useCache: boolean) {
    trace.length = 0;
    const context = new HttpContext().set(CACHE, useCache);
    this.http.get<{ n: number }>("/api/items", { context }).subscribe((r) => {
      this.log.set(`body.n=${r.n} | ${trace.join(" > ")}`);
    });
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 21 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

Explain what output() and outputFromObservable() are in modern Angular and how they differ from @Output().

Continue