Angular – HTTP Interceptors

July 18, 20263 min readUpdated 8/21/2026

An interceptor is a function every outgoing request passes through. It is the one place that knows where the API lives, how the token is attached, and how a failure becomes a typed error.

export const apiInterceptor: HttpInterceptorFn = (req, next) => {

Registered once, at bootstrap:

provideHttpClient(withFetch(), withInterceptors([apiInterceptor])),

Why an interceptor and not a wrapper function

The React app hand-writes a request() wrapper around fetch that does the same three jobs. The difference is reach: a wrapper only helps code that remembers to call it, and nothing stops a component calling fetch directly and skipping all three. An interceptor is wired into HttpClient itself, so there is no way to make a request that bypasses it.

Requests are immutable

const authorised = req.clone({
  url: `${environment.apiBaseUrl}${req.url}`,
  setHeaders: token ? { Authorization: `Bearer ${token}` } : {},
});

⚠️ req.url = … does nothing at all, silently. Every change goes through clone(), which is why both edits are made in one call.

Only touch your own API

const isOurApi = req.url.startsWith('/api/');
if (!isOurApi) return next(req);

This is a security boundary, not tidiness. A request for an asset, or to Stripe, must pass through untouched — sending your bearer token to a third party would be a credential leak. An interceptor sees every request the app makes, including ones to other origins, so the check has to be explicit.

Its own unit test asserts exactly that: with a token stored, a request to api.stripe.com carries no Authorization header.

Attach the token unconditionally

The token is attached whenever one exists, rather than per-call as the React app does with an auth: true flag. The behaviour is the same — a guest has no token to send — and it removes a class of bug where a protected endpoint is called without the flag and 401s.

Guest checkout still works, because POST /api/orders is deliberately open and the server associates the order with an account only when a valid token happens to be present.

Convert the error once

return next(authorised).pipe(
  catchError((error: HttpErrorResponse) =>

Every caller downstream — a component, a service, an NgRx effect — then handles exactly one error type and can read fieldErrors() off it without first unpicking Angular's transport wrapper.

The conversion also translates the case that produces the worst message. Status 0 means the request never reached the server at all — the API is down, or CORS rejected it — and saying so beats "Http failure response for …: 0 Unknown Error".

Order matters

Interceptors run in the order given to withInterceptors, wrapping each other like middleware: the first sees the request first and the response last. An interceptor that adds auth must run before one that logs the final request; a retry interceptor must sit outside the one that converts errors, or it will retry on a type it no longer recognises. With one interceptor this does not come up — with three it is the first thing to check when something behaves oddly.

The class-based form

You will meet @Injectable() classes implementing HttpInterceptor, registered through the HTTP_INTERCEPTORS multi-provider. It still works. A plain function needs neither, and can still use inject() if it wants a service.

What is next

The RxJS that survives in a signals-first app, and the one place it is unambiguously the right tool.