skip to content

Request & Response Interceptors

Functional HttpInterceptorFn interceptors wrap each request, clone it to add headers and pipe the response through retry or error mapping. Interviewers ask about chain order and auth tokens.

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

explore

questions

5

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

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, 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

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

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