skip to content

An Angular app adds provideHttpClient(withXhr()) to a lazy uploads route's providers, and the root auth interceptor stops running there; why, and how do you fix it?

level: seniorimportance: nice to knowfreq 22%

answer

  1. a new injector, a new client
  2. configuration overrides the parent
  3. delegation has a restriction
  4. re-register what you need

basics

~20 s

provideHttpClient() in a route's providers creates an independent HttpClient whose configuration replaces the parent's, so root interceptors do not run. withRequestsMadeViaParent() would delegate upward but cannot be combined with withXhr(), so re-register the interceptors in the route's provideHttpClient call.

solid answer

~40 s

A lazy route's `providers` form their own environment injector, and `provideHttpClient()` there creates a **separate** `HttpClient` configuration that **overrides** the parent one: it has its own interceptor list, its own XSRF interceptor and its own backend. The root's `withInterceptors([authInterceptor])` therefore never sees requests made by services resolved in that route. The usual cure for nested clients is `withRequestsMadeViaParent()`, which passes requests through the local interceptors and then into the parent client, but it takes the parent's backend and Angular rejects combining it with `withXhr()` or `withFetch()` (a development-mode configuration error). So for an XHR-only upload area, repeat the interceptors: `provideHttpClient(withXhr(), withInterceptors([authInterceptor]))`. Alternatives: switch the whole app to `withXhr()` if it has no SSR, or keep fetch and show indeterminate progress.

code

ts · 19 lines
ts
// app.config.ts: the root client with the auth interceptor
providers: [provideHttpClient(withInterceptors([authInterceptor]))]

// app.routes.ts: a lazy upload area that needs XHR for progress events
import {Routes} from '@angular/router';
import {provideHttpClient, withInterceptors, withXhr} from '@angular/common/http';
import {authInterceptor} from './auth.interceptor';

export const routes: Routes = [
  {
    path: 'uploads',
    loadComponent: () => import('./uploads/uploads-page').then((m) => m.UploadsPage),
    providers: [
      // an independent client: the root interceptors do NOT apply here,
      // and withRequestsMadeViaParent() cannot be combined with withXhr()
      provideHttpClient(withXhr(), withInterceptors([authInterceptor])),
    ],
  },
];

go deeper

for a junior

Know that provideHttpClient() can also appear in a route's providers and that it then creates a separate client for that area.

for a middle

Explain that a nested configuration overrides rather than extends the parent's, and which injector a service's HttpClient comes from.

for a senior

Diagnose missing interceptors in a lazy area, know that withRequestsMadeViaParent cannot be combined with withXhr, and pick between repeating interceptors, global XHR or indeterminate progress.

for a principal

Decide whether feature areas may own HTTP configuration at all, and how shared interceptors are kept in sync if they do.

## Clients follow the injector tree `provideHttpClient()` returns environment providers, and every environment injector that receives them gets its **own** `HttpClient` configuration: its own `HttpClient`, handler, interceptor list and backend. Lazy routes with a `providers` array create such an injector. Code that injects `HttpClient` from that injector, or from a component below it, gets the nearest configuration. By default a nested configuration **replaces** the parent's rather than extending it: - interceptors registered at the root do not run for the nested client; - the nested client registers its own copy of the built-in XSRF interceptor; - its backend is whatever its own features select (fetch unless `withXhr()` is passed). That is why the auth header disappears only inside the uploads area. ## Which requests use which client A service decides which client it gets by **where it is provided**: | Service provided in | Injects the client from | |---|---| | `providedIn: 'root'` | the root injector, even when called from the uploads page | | the route's `providers` | the route's injector, i.e. the nested XHR client | | a component inside the route | the nearest environment injector above it: the route's | So a root-provided `UploadService` would not even use the route's XHR client; the upload service must be provided in the route (or injected in a component under it) for the nested configuration to matter. ## Delegating to the parent: withRequestsMadeViaParent() `withRequestsMadeViaParent()` changes the nested client to pass each request, after its own interceptors, **into the parent's `HttpClient` chain** instead of to a backend. Requests then run through both interceptor lists and use the parent's backend. Several levels can chain this way until a client without the feature is reached. Two restrictions matter: 1. The parent must configure `HttpClient`; otherwise using the nested client is a runtime error in development mode. 2. It **cannot be combined with `withFetch()` or `withXhr()`** in the same `provideHttpClient()` call; Angular throws a configuration error, because a delegating client never uses a backend of its own. That second rule is exactly what blocks "XHR for uploads, root interceptors for everything". ## Workable designs 1. **Repeat the interceptors locally:** `provideHttpClient(withXhr(), withInterceptors([authInterceptor]))` in the route. Simple, but every root interceptor must be listed again, and a new root interceptor must be added in both places. 2. **Use XHR everywhere:** `provideHttpClient(withXhr(), withInterceptors([...]))` once at the root. Fine for a browser-only app; not for SSR, where server XHR is deprecated and planned for removal in v23. 3. **Stay on fetch:** keep one client and show an indeterminate indicator instead of a percentage for uploads. ## Choosing between the designs | Design | Keeps root interceptors | Upload progress | Safe with SSR | |---|---|---|---| | repeat interceptors in the route client | yes, if listed again | yes | only if the route never renders on the server | | `withXhr()` for the whole app | yes | yes | no | | one fetch client everywhere | yes | no (indeterminate indicator) | yes | | `withRequestsMadeViaParent()` | yes | no: it cannot take `withXhr()` | yes | ## The NgModule version of the same trap Module-based apps hit the same behaviour when a lazily loaded `NgModule` imports `HttpClientModule`: that import configures a client for the module's injector, and the Angular docs warn that with `HttpClientModule` in several injectors the interceptor behaviour is poorly defined and depends on import order. Replacing those imports with `provideHttpClient()` makes the override explicit, and `withRequestsMadeViaParent()` makes delegation explicit. ## Diagnosing it quickly - Log inside the interceptor: if it never fires for the uploads requests, a nested configuration is in play. - Search the route definitions and `NgModule`s for `provideHttpClient` or `HttpClientModule` imports below the root. - Check where the service that makes the request is provided. ## Why Angular designs it this way Overriding by default keeps a feature area's HTTP behaviour self-contained: a lazily loaded area can be tested and reasoned about with only the configuration it declares. Delegation is opt-in because doubling interceptors (running an auth or retry interceptor at two levels) is its own class of bug.

  • The route-level client is configured, but uploads still go through fetch. What is the likely cause?
    The service making the request is `providedIn: 'root'`, so it was created by the root injector and injected the root client. Provide the upload service in the route's `providers` (or inject `HttpClient` in a component under the route) so it resolves the nested XHR client.
  • When is withRequestsMadeViaParent() the right tool?
    When a feature area only needs to add interceptors, for example a feature-specific header, while keeping everything the root does, including its backend. The request runs the local interceptors, then the parent chain. It is not usable when the area needs a different backend.

saying these in an interview costs you the question

  • A nested provideHttpClient() inherits the root interceptors automatically
  • withRequestsMadeViaParent() can be combined with withXhr() to change only the backend
  • Any service injected from the uploads page uses the route's HttpClient, even if providedIn root
  • provideHttpClient() in a route's providers is a no-op if the root already configures one