skip to content

After an Angular app replaces HttpClientModule with provideHttpClient(), its class-based AuthInterceptor stops adding headers; why, and what fixes it?

level: middleimportance: should knowfreq 42%

answer

  1. the token nobody reads
  2. the module did it implicitly
  3. a feature to opt in
  4. or convert to a function

basics

~10 s

HttpClientModule read class interceptors from the HTTP_INTERCEPTORS token; provideHttpClient() only does so when given withInterceptorsFromDi(). Add that feature, or rewrite the interceptor as an HttpInterceptorFn registered with withInterceptors([...]), the recommended form.

solid answer

~40 s

`HttpClientModule` is equivalent to `provideHttpClient(withInterceptorsFromDi(), withXhr())`, so it silently included every class registered with `{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}`. A bare `provideHttpClient()` does **not** look at `HTTP_INTERCEPTORS`; functional interceptors come only from `withInterceptors([...])`, and class-based ones only when `withInterceptorsFromDi()` is passed. So the provider is still there but nothing reads it, and requests go out without the header. Fix it by adding `withInterceptorsFromDi()`, or better, by converting the class to an `HttpInterceptorFn` and registering it with `withInterceptors([authInterceptor])`, which has more predictable ordering. Also check the backend: the module used `withXhr()`, so the migrated app now runs on `FetchBackend` unless you keep `withXhr()`.

code

ts · 26 lines
ts
// auth.interceptor.ts: an existing class-based interceptor
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {inject, Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {TokenStore} from './token-store';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private readonly tokens = inject(TokenStore);

  intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(req.clone({setHeaders: {Authorization: `Bearer ${this.tokens.current()}`}}));
  }
}

// app.config.ts: after migrating off HttpClientModule
import {ApplicationConfig} from '@angular/core';
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    // without withInterceptorsFromDi() the HTTP_INTERCEPTORS entry below is never read
    provideHttpClient(withInterceptorsFromDi()),
    {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
  ],
};

go deeper

for a junior

Know the two registration routes: withInterceptors([...]) for functions and HTTP_INTERCEPTORS plus withInterceptorsFromDi() for classes.

for a middle

Explain why HttpClientModule included DI interceptors implicitly and what a bare provideHttpClient() leaves out, including the backend change.

for a senior

Run the migration safely: inventory HTTP_INTERCEPTORS providers, choose between withInterceptorsFromDi and conversion, and keep the backend change a separate step.

for a principal

Plan retiring class-based interceptors across many libraries and teams, given that DI-provided interceptor support may be phased out.

## Two ways to register interceptors `HttpClient` supports two interceptor styles, and each is registered differently: | Style | Shape | Registered with | |---|---|---| | **functional** | `HttpInterceptorFn`, a plain function `(req, next) => ...` | `provideHttpClient(withInterceptors([fn1, fn2]))` | | **class-based** | a class implementing `HttpInterceptor` with `intercept(req, next)` | `{provide: HTTP_INTERCEPTORS, useClass: X, multi: true}` **plus** `withInterceptorsFromDi()` | The class-based style comes from the NgModule era. The `HTTP_INTERCEPTORS` multi-provider token only collects the classes; something has to read the token and splice those classes into the chain. That something is the `withInterceptorsFromDi()` feature. ## Why the migration broke the header `HttpClientModule` is defined as `provideHttpClient(withInterceptorsFromDi(), withXhr())`. An app importing it therefore had the DI-interceptor feature switched on without ever naming it. A typical migration rewrites the import to a bare `provideHttpClient()`: 1. the `HTTP_INTERCEPTORS` provider is still registered; 2. but no feature reads the token; 3. so `AuthInterceptor` is never instantiated and requests leave without `Authorization`. Nothing throws; the symptom is a wave of 401 responses. ## The two fixes - **Minimal:** pass `withInterceptorsFromDi()` to `provideHttpClient()`. The existing class and provider start working again. - **Recommended:** convert to a functional interceptor and register it with `withInterceptors([authInterceptor])`. The Angular docs recommend functional interceptors for their more predictable ordering, and the source notes that support for DI-provided interceptors may be phased out in a later release. Both styles can coexist during a migration. Their relative order follows the order in which the features are passed to `provideHttpClient()`, so `provideHttpClient(withInterceptors([a]), withInterceptorsFromDi())` runs `a` before the class-based ones. ## What the functional version looks like Converting is mostly mechanical: the `intercept` method body becomes a function, dependencies come from `inject()`, and `next.handle(req)` becomes `next(req)`. ```ts export const authInterceptor: HttpInterceptorFn = (req, next) => next(req.clone({setHeaders: {Authorization: `Bearer ${inject(TokenStore).current()}`}})); ``` It is then registered once, `provideHttpClient(withInterceptors([authInterceptor]))`, and the `HTTP_INTERCEPTORS` provider is deleted. Deleting it matters: if both registrations remain and `withInterceptorsFromDi()` is also present, the header logic runs twice. ## Finding every class interceptor before you migrate - search for `HTTP_INTERCEPTORS` across the app **and** its libraries; library `NgModule`s often register interceptors in their `forRoot()`; - check lazily loaded modules that import `HttpClientModule` themselves, since each import configured a separate client; - list the interceptors in the order they were provided, because that order becomes the chain order under `withInterceptorsFromDi()`. ## The second thing the migration changed The module also carried `withXhr()`. After replacing it with a bare `provideHttpClient()` in v22, the app switches from `HttpXhrBackend` to `FetchBackend`. Most code does not notice, but: - upload-progress bars need `withXhr()` back (on fetch, `reportUploadProgress` throws NG02824); - any code relying on XHR-specific behaviour should be retested. The faithful one-to-one replacement is therefore `provideHttpClient(withInterceptorsFromDi(), withXhr())`; drop each feature deliberately, not by accident. ## Why the provider functions replaced the modules The NgModules bundled several decisions into one import: which backend to use, whether to read DI interceptors, and XSRF settings. The provider functions make each decision an explicit, named feature, which suits standalone applications (there is no `NgModule` to import into), keeps unused features out of the bundle, and lets Angular validate combinations. They also behave predictably across injectors, where the module form did not. ## Migration checklist - search for `HTTP_INTERCEPTORS` providers before removing `HttpClientModule`; - add `withInterceptorsFromDi()` or convert those classes to functions; - decide explicitly between the fetch default and `withXhr()`; - replace `HttpClientXsrfModule` options with `withXsrfConfiguration(...)` or `withNoXsrfProtection()`; - keep the configuration in one environment injector: `HttpClientModule` imported into several injectors had poorly defined interceptor behaviour, which is one reason the provider functions replaced it.

  • In what order do a functional interceptor and the class-based ones run with provideHttpClient(withInterceptors([a]), withInterceptorsFromDi())?
    Registration order is chain order, and the features contribute their entries in the order they are passed, after the built-in XSRF interceptor. So the request passes the XSRF interceptor, then `a`, then the class-based interceptors in their `HTTP_INTERCEPTORS` order, then the backend; responses flow back in reverse.
  • Why might a team keep withXhr() when replacing HttpClientModule?
    `HttpClientModule` used the XMLHttpRequest backend. Keeping `withXhr()` makes the migration behaviour-neutral, which matters if the app shows upload progress. The switch to the fetch default can then be a separate, tested change.

saying these in an interview costs you the question

  • provideHttpClient() automatically picks up every HTTP_INTERCEPTORS provider
  • Class-based interceptors are no longer supported in Angular 22
  • withInterceptorsFromDi() is how you register functional interceptors
  • Replacing HttpClientModule with a bare provideHttpClient() changes nothing else
  • The missing header means the interceptor threw and Angular swallowed the error