skip to content

HTTP Client

HttpClient from @angular/common/http sends requests as typed Observables, runs them through functional interceptors and feeds signals via httpResource. Interviewers probe setup and interceptors.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

18

In Angular, how do you write an HttpInterceptorFn that attaches a bearer token to API requests, and why must it clone the request?

level: juniorimportance: must knowfreq 72%

answer

  1. a function between HttpClient and the backend
  2. requests are read-only
  3. setHeaders on a copy
  4. only your own API

basics

~20 s

An HttpInterceptorFn receives the outgoing HttpRequest and a next function. Because HttpRequest is immutable, it builds a copy with req.clone({ setHeaders: { Authorization: 'Bearer ...' } }) and returns next(copy), reading the token through inject() and skipping URLs outside its own API.

solid answer

~40 s

A functional interceptor has the type `HttpInterceptorFn`: `(req: HttpRequest<unknown>, next: HttpHandlerFn) => Observable<HttpEvent<unknown>>`. It runs in an injection context, so it can call `inject(AuthService)` to read the current token. `HttpRequest` is immutable — its properties are read-only and its headers are an immutable `HttpHeaders` — so the interceptor creates a modified copy with `req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })` and passes that copy to `next()`. Returning `next(req)` unchanged forwards the original. Immutability is what makes the chain safe: the same request can be retried or passed through several interceptors without one silently changing another's view. Two production details: attach the token only to your own API's URLs, so it never leaks to third parties, and register the function with `provideHttpClient(withInterceptors([authInterceptor]))`.

code

ts · 24 lines
ts
import { ApplicationConfig, Injectable, inject, signal } from '@angular/core';
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly accessToken = signal<string | null>(null);
}

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  // Read per request, inside the injection context Angular provides.
  const token = inject(AuthService).accessToken();

  // Never send the token to other origins.
  if (!token || !req.url.startsWith('/api/')) {
    return next(req);
  }

  // HttpRequest is immutable: forward a modified copy.
  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([authInterceptor]))],
};

go deeper

for a junior

Write the HttpInterceptorFn from memory: inject the auth service, clone with setHeaders, return next(copy), and register it with withInterceptors.

for a middle

Explain why HttpRequest is immutable, what clone() accepts, and why inject() only works synchronously in the interceptor body.

for a senior

Scope the token to the right origins, read it per request so refreshed tokens apply, and decide how the same interceptor handles 401 responses.

for a principal

Decide whether authentication belongs in HttpClient interceptors, in a gateway or in cookie-based sessions, weighing token exposure to scripts against implementation cost.

## What an interceptor is An **interceptor** in Angular's `HttpClient` is middleware: code that runs for every request made through `HttpClient`, between your service call and the **backend** that actually talks to the network. Since standalone APIs became the norm, the recommended form is a plain function of type **`HttpInterceptorFn`**: ```ts type HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => Observable<HttpEvent<unknown>>; ``` - `req` is the outgoing **`HttpRequest`**: method, URL, headers, params, body, context. - `next` is an **`HttpHandlerFn`** that passes a request to the next interceptor, or to the backend when this is the last one. - The return value is the Observable of `HttpEvent`s the rest of the chain produces, which the interceptor may also transform. ## Why the request must be cloned `HttpRequest` is **immutable**. Its fields are `readonly`, and its `headers` and `params` are the immutable `HttpHeaders` and `HttpParams` types, whose `set()` returns a new instance. Two consequences: 1. Writing `req.headers.set('Authorization', value)` and then `return next(req)` sends the request *without* the header — `set()` built a new `HttpHeaders` that was thrown away. 2. The only way to change a request is `req.clone(update)`, which returns a new `HttpRequest` with the listed fields replaced. `clone()` accepts the ordinary request options plus conveniences: **`setHeaders`** sets (replaces) each listed header, **`setParams`** sets each listed query parameter, and `url`, `method` and `body` replace those fields. The Angular guide explains why the design is immutable: an interceptor can be handed the same request more than once — for example when a request is retried — and immutability keeps that repeatable. ## Writing the bearer-token interceptor ```ts export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthService).accessToken(); if (!token || !req.url.startsWith('/api/')) { return next(req); } return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); }; ``` Step by step: 1. **`inject(AuthService)`** works because Angular calls each functional interceptor inside `runInInjectionContext` for the injector that configured it. The call must happen synchronously in the function body, not later inside an RxJS callback. 2. **Guard the destination.** An app also calls third-party APIs, analytics endpoints and CDNs through `HttpClient`; sending your access token to them leaks a credential. Match your own origin or path prefix explicitly. 3. **Skip when there is no token**, so anonymous calls go through unchanged. 4. **Clone once and forward** the copy with `next()`. ## Registering it The function does nothing until it is part of the `HttpClient` configuration: ```ts export const appConfig: ApplicationConfig = { providers: [provideHttpClient(withInterceptors([authInterceptor]))], }; ``` The array order is the chain order: the first interceptor sees the request first. ## Functional versus class-based interceptors | | `HttpInterceptorFn` | Class implementing `HttpInterceptor` | |---|---|---| | Shape | A function `(req, next)` | An `@Injectable` class with `intercept(req, next: HttpHandler)` | | Forwarding | `next(req)` | `next.handle(req)` | | Dependencies | `inject()` in the body | Constructor injection or `inject()` in fields | | Registration | `withInterceptors([...])` | `HTTP_INTERCEPTORS` multi-provider plus `withInterceptorsFromDi()` | | Guide's recommendation | Preferred | Supported, order harder to predict | Class-based interceptors are still in many codebases; the logic inside them is the same. ## Reading the token at the right moment The token should be read **per request**, inside the interceptor, from a service that always holds the current value — a signal or a plain field on an auth service. Capturing it in a module-level constant, or in a closure created once at start-up, freezes the first token forever, and a later refresh or login never reaches outgoing requests. The same applies to other per-user headers such as a tenant or locale header: compute them when the request passes through, not when the interceptor is defined. ## Common mistakes - Calling `req.headers.set(...)` and forwarding the original `req`. - Attaching the token to every URL, including other origins. - Calling `inject()` inside `pipe(catchError(...))` instead of at the top of the function. - Forgetting to register the function, so it never runs. - Reading the token once at module load time instead of per request, so a refreshed token is never picked up.

  • What happens if the interceptor calls req.headers.set('Authorization', value) and then returns next(req)?
    The request goes out without the header. `HttpHeaders` is immutable, so `set()` returned a new headers object that was discarded, and `req` itself was never changed. The fix is to forward a clone: `next(req.clone({ setHeaders: { Authorization: value } }))`, or `req.clone({ headers: req.headers.set('Authorization', value) })`.
  • Why can an Angular functional interceptor call inject() at the top of its body but not inside a catchError callback?
    Angular invokes each functional interceptor with `runInInjectionContext` for the injector that configured it, and that context exists only while the function body runs synchronously. A `catchError` or `switchMap` callback runs later, when a response or error arrives, outside the context, so `inject()` there fails with NG0203. Inject everything you need at the top and close over it.
  • An interceptor sends the token to /api/ URLs only. Why is that check needed in a security review?
    The same `HttpClient` also calls third-party services, and an interceptor runs for all of them. Without a destination check, the user's access token would be sent to every host the app talks to, handing a credential to parties that should never hold it. Matching your own origin or path prefix, ideally from configuration, keeps it scoped.

saying these in an interview costs you the question

  • You can set a header directly on req.headers and pass the same req to next().
  • An interceptor should attach the bearer token to every outgoing request, whatever its host.
  • Functional interceptors cannot use dependency injection, so the token must be a global variable.
  • Interceptors run automatically once the function is exported; no registration is needed.
  • The request passed to an interceptor can be modified by assigning req.url.
open as a page

With Angular's HttpClient, why does a WeatherService method returning http.get<Forecast>() send no request, and when does it send two?

level: juniorimportance: must knowfreq 74%

basics

~20 s

HttpClient methods return cold Observables: building one sends nothing, every subscription sends its own request, and unsubscribing before the response arrives aborts it. A method that only returns the Observable stays silent until something subscribes.

open as a page

In Angular, when does httpResource() send a request for a reviews panel keyed by a productId signal, and what does it expose?

level: juniorimportance: must knowfreq 55%

basics

~20 s

httpResource() tracks the signals its request function reads and sends the request eagerly, then again whenever one of them changes, cancelling any pending request. Returning undefined keeps it idle. It exposes value, status, error, isLoading, headers and statusCode as signals.

open as a page

In a standalone Angular 22 app, how is HttpClient made available, and when do you still need provideHttpClient()?

level: juniorimportance: must knowfreq 66%

basics

~10 s

Since Angular 21, HttpClient is provided in root, so inject(HttpClient) works with no setup. provideHttpClient() in the application providers is still how you add features: withInterceptors, withInterceptorsFromDi, withXsrfConfiguration or withNoXsrfProtection, withXhr and withRequestsMadeViaParent.

open as a page

With Angular's HttpClient, how are a 404, a network failure and an unparseable 200 response each reported, and what does status 0 mean?

level: middleimportance: must knowfreq 62%

basics

~20 s

All three reach the error channel as an HttpErrorResponse. A 404 keeps status 404 and the error body in error; status 0 means no HTTP response reached the app (offline, CORS block, timeout); an unparseable 200 keeps status 200 and a parsing message.

open as a page

In an Angular app, how should an auth interceptor refresh an expired access token on a 401 and replay the request without five concurrent failures triggering five refreshes?

level: seniorimportance: must knowfreq 56%

basics

~20 s

In the interceptor, catch an HttpErrorResponse with status 401, wait on one shared in-flight refresh Observable, then call next() again with a clone carrying the new token. The refresh request itself skips the interceptor, and a rejected refresh logs the user out.

open as a page

In Angular's HttpClient, why does calling params.set('units', 'metric') on an HttpParams object leave the request's query string unchanged?

level: juniorimportance: should knowfreq 52%

basics

~20 s

HttpParams and HttpHeaders are immutable: set(), append() and delete() return a new instance and leave the original untouched. Discarding the return value discards the change, so reassign the result, chain the calls, or pass a plain object literal instead.

open as a page

In Angular, given provideHttpClient(withInterceptors([a, b, c])), in what order do the interceptors see the request and the response, and why does placement matter?

level: middleimportance: should knowfreq 48%

basics

~20 s

The request passes a, then b, then c, then the backend; the response and errors travel back through c, b, then a. So a sees the final outcome last, and each interceptor sees only request changes made by those listed before it.

open as a page

With Angular's HttpClient, how do the observe and responseType options change what http.get() emits, for example when you need a response header?

level: middleimportance: should knowfreq 48%

basics

~20 s

observe picks what each emission is: the body (default), the full HttpResponse with status and headers, or the HttpEvent stream. responseType picks how the body is read: json (default), text, blob or arraybuffer. Both need literal values to select the typed overload.

open as a page

In Angular, what does httpResource's parse option do that httpResource<Review[]>() alone does not, and how does it work with httpResource.text() or blob()?

level: middleimportance: should knowfreq 36%

basics

~20 s

httpResource<Review[]>() only asserts a type. parse receives the raw body (unknown for JSON, string for text(), Blob for blob()), validates or transforms it, and its return type becomes the value type; a throw puts the resource in error.

open as a page

In Angular, how should a template read an httpResource's value, error and statusCode so a 404 for an unknown product does not crash it?

level: middleimportance: should knowfreq 45%

basics

~20 s

Check error() or hasValue() before reading value(), because value() throws while the resource is in the error state. For a 404, error() holds the HttpErrorResponse and statusCode() returns 404, so the template can show a not-found message.

open as a page

Since Angular 22, which backend does HttpClient use by default, and why would a file-upload feature need withXhr()?

level: middleimportance: should knowfreq 48%

basics

~20 s

Since v22 HttpClient uses FetchBackend, built on the fetch API, and withFetch() is deprecated. Fetch cannot report upload progress, so an upload bar with reportUploadProgress needs provideHttpClient(withXhr()), which switches to HttpXhrBackend; on FetchBackend that option fails with NG02824.

open as a page

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%

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.

open as a page

In Angular, how would you write interceptors that retry transient HTTP failures and map errors centrally, without retrying unsafe requests or swallowing errors?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A retry interceptor applies RxJS retry({ count, delay }) to next(req) only for safe methods and transient statuses (0, 502, 503, 504), with backoff. An outer error interceptor reports or maps the final HttpErrorResponse and rethrows it instead of returning an empty stream.

open as a page

An Angular WeatherService uses http.get<Forecast>() and compiles, yet forecast.issuedAt.getHours() throws in production; why did the type not catch it, and how do you harden it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The generic in http.get<Forecast>() is an unchecked assertion: HttpClient parses the JSON and passes it on without checking it, and JSON has no Date. Type the wire format honestly, validate it in the service, and map it to domain types there.

open as a page

In an Angular 22 app, when would you fetch the reviews panel's data with httpResource, and when with HttpClient plus toSignal() or rxResource() instead?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Use httpResource for signal-keyed GET reads shown in a view. Use HttpClient directly for writes, rxResource when the loader must be an Observable pipeline or an existing service method, and toSignal for one fixed stream that does not refetch when inputs change.

open as a page

In Angular, how can one HttpClient call tell an interceptor to skip adding the bearer token, using an HttpContextToken instead of URL matching?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Define a key such as new HttpContextToken<boolean>(() => false), pass context: new HttpContext().set(SKIP_AUTH, true) on that call, and have the interceptor check req.context.get(SKIP_AUTH). The context travels with the request to interceptors but is never sent to the server.

open as a page

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%

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.

open as a page