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?
answer
- a new injector, a new client
- configuration overrides the parent
- delegation has a restriction
- re-register what you need
basics
~20 sprovideHttpClient() 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 sA 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// 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
Know that provideHttpClient() can also appear in a route's providers and that it then creates a separate client for that area.
Explain that a nested configuration overrides rather than extends the parent's, and which injector a service's HttpClient comes from.
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.
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