skip to content

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.