Tutorials

HTTP Interceptor Angular

Key Takeaways

  • Use functional interceptors with provideHttpClient(withInterceptors([...])) for new configurations; class-based interceptors remain supported.
  • Clone HttpRequest before changing its URL or headers. Cloning does not protect nested request or response bodies from mutation.
  • Requests travel through interceptors in registration order; response processing unwinds in reverse order.
  • Scope authorization headers to approved API endpoints, and treat caching as an explicit freshness and privacy policy.
  • When an interceptor isn't called, check registration, injector scope, and observable subscription—not an imaginary rule that interceptors must be provided before HttpClient.

An Angular HTTP interceptor centralizes behavior around requests made through a configured HttpClient, including authorization headers, URL changes, logging, and response caching. Use an interceptor for behavior shared across requests; keep endpoint-specific business logic in your API services. Angular recommends functional interceptors because their ordering is more predictable in complex configurations.

If you need the request basics first, start with our Angular HttpClient tutorial.

How the Angular HTTP Interceptor Chain Works

An interceptor receives an outgoing request and the next handler. Calling next(req) forwards a functional interceptor's request; the returned observable carries HTTP events back through the chain. An observable represents values delivered over time—see our JavaScript observables introduction for the underlying model.

Registered: [logging, auth, cache]

Request:  caller --> logging --> auth --> cache --> backend
Response: caller <-- logging <-- auth <-- cache <-- backend
                                             |
                                  cache hit: return a response
                                  without calling the backend

For this configuration, logging sees the outgoing request first and processes returning responses last. A cache can short-circuit the remaining chain by returning its own response observable instead of calling next. Class-based interceptors follow provider registration order on the request path, but hierarchical dependency injection can make that order harder to reason about.

Response interception means working with the observable returned by the next handler. Its events can include request dispatch, headers, progress, and the final response; don't assume every event has a response body. Check event instanceof HttpResponse or event.type === HttpEventType.Response before accessing final-response properties.

Create an Angular Functional Interceptor

Add an Authorization Header and Log Requests

The following browser-only example approves HTTPS requests to the current origin under /api/. Your sign-in flow supplies the current access token through AuthSession; the interceptor doesn't capture a token during application startup. The Bearer prefix must match your server's authentication scheme.

// src/app/http-interceptors.ts
import { Injectable, inject, isDevMode } from '@angular/core';
import {
  HttpContextToken,
  HttpErrorResponse,
  HttpHandlerFn,
  HttpInterceptorFn,
  HttpRequest,
  HttpResponse,
} from '@angular/common/http';
import { catchError, tap, throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthSession {
  accessToken: string | null = null;
}

export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);

export const authInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn,
) => {
  const token = inject(AuthSession).accessToken;
  const target = new URL(req.url, document.baseURI);
  const approved =
    target.protocol === 'https:' &&
    target.origin === window.location.origin &&
    target.pathname.startsWith('/api/');

  if (
    !approved ||
    !token ||
    req.context.get(SKIP_AUTH) ||
    req.headers.has('Authorization')
  ) {
    return next(req);
  }

  return next(req.clone({
    setHeaders: { Authorization: `Bearer ${token}` },
  }));
};

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const debug = isDevMode();
  const target = new URL(req.url, document.baseURI);
  const label = `${req.method} ${target.origin}${target.pathname}`;

  if (debug) console.log(label);

  return next(req).pipe(
    tap(event => {
      if (debug && event instanceof HttpResponse) {
        console.log(label, event.status);
      }
    }),
    catchError((error: unknown) => {
      if (debug && error instanceof HttpErrorResponse) {
        console.warn(label, error.status);
      }
      return throwError(() => error);
    }),
  );
};

HttpRequest is immutable: use req.clone() to change headers, parameters, or the URL. Here, setHeaders changes the authorization value on a new request while retaining its other configuration. Request and response bodies aren't deeply immutable, so don't modify nested objects in place—especially when requests may be retried.

HttpContextToken provides request metadata for interceptors rather than an extra header sent to the server. The example also preserves an explicitly supplied authorization header. For a separate API host, replace the same-origin check with a deployment-configured, exact approved origin; don't use substring matching.

Register Interceptors in a Standalone Application

In an existing Angular workspace, configure the interceptor functions in app.config.ts and pass that configuration to bootstrapApplication. Keep any existing router or other application providers alongside these providers.

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor, loggingInterceptor } from './http-interceptors';

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

This minimal root component makes a request when you press the button. It assumes the workspace's HTML contains the usual app-root host; replace /api/ping with an endpoint your backend serves.

// src/main.ts
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <button (click)="load()">Send request</button>
    <p>{{ status() }}</p>
  `,
})
class AppComponent {
  private readonly http = inject(HttpClient);
  readonly status = signal('Ready');

  load(): void {
    this.status.set('Loading');
    this.http.get('/api/ping', { responseType: 'text' }).subscribe({
      next: body => this.status.set(body),
      error: () => this.status.set('Request failed'),
    });
  }
}

bootstrapApplication(AppComponent, appConfig)
  .catch(error => console.error('Bootstrap failed', error));

The session starts without a token, so requests initially pass through without an authorization header. Populate AuthSession.accessToken after successful sign-in and clear it on sign-out. This example deliberately won't attach a token over plain HTTP.

Class-Based HttpInterceptor and HTTP_INTERCEPTORS

The class-based pass-through example is still a useful starting point. Implement intercept() and return next.handle(req); a functional interceptor uses next(req) instead.

// src/app/pass-through.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class PassThroughInterceptor implements HttpInterceptor {
  intercept(
    req: HttpRequest<unknown>,
    next: HttpHandler,
  ): Observable<HttpEvent<unknown>> {
    return next.handle(req);
  }
}

For an NgModule-based application, add the following providers to the existing module. withInterceptorsFromDi() connects the class-based multi-provider to that HttpClient configuration; multi: true collects multiple interceptor providers rather than replacing the token's value.

// app.module.ts fragment: retain your existing module metadata.
import { NgModule } from '@angular/core';
import {
  HTTP_INTERCEPTORS,
  provideHttpClient,
  withInterceptorsFromDi,
} from '@angular/common/http';
import { PassThroughInterceptor } from './pass-through.interceptor';

@NgModule({
  // ... existing imports, declarations, and bootstrap configuration
  providers: [
    provideHttpClient(withInterceptorsFromDi()),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: PassThroughInterceptor,
      multi: true,
    },
  ],
})
export class AppModule {}

HttpClientModule remains in older applications, but it is deprecated; use provideHttpClient for new configuration, including inside NgModules. Class-based interceptors remain supported, while Angular recommends functional interceptors for new work.

Modify Request URLs Without Rewriting the Internet

The old recommendation to replace every http:// with https:// was too broad. Configure HTTPS API base URLs and enforce HTTPS at your server or edge instead. An interceptor shouldn't silently change third-party destinations or intentionally local development endpoints.

If a migration genuinely requires URL rewriting, resolve relative URLs against the correct application base using new URL(req.url, base), compare the parsed origin against an explicit allowlist, and clone only requests covered by that policy. Preserve the intended path and query parameters, and perform routing before an authorization interceptor decides whether to attach credentials. The URL constructor resolves relative references and throws for invalid URLs.

Angular HTTP Interceptor Caching: Define the Policy First

Browser Caching Versus an Interceptor Cache

Prefer standard HTTP caching when it meets your requirements. Browser caches use server directives such as Cache-Control and validators such as ETag; conditional requests can validate an existing representation without downloading its body again. An interceptor cache is application code that must implement its own eligibility, expiry, and invalidation rules.

ConcernBrowser HTTP cacheInterceptor cache
FreshnessHTTP caching directives and validationYour time-to-live and invalidation policy
Request identityHTTP cache matching, including VaryYour explicit cache key
Serving a cached responseHandled below the interceptor layerCan skip later interceptors and the backend

A Safe In-Memory Cache Design

Don't replace the old localStorage snippet with another universal cache. For example, cache only an explicitly approved public catalog GET for 30 seconds, with a bounded entry count and invalidation after catalog changes. Those are example application policies, not Angular defaults.

  1. Restrict eligibility. Require an approved endpoint and an explicit opt-in, such as a context token defaulting to false. Reject non-GET requests and responses outside your approved successful status policy. An absent authorization header doesn't prove a response is public: cookies may authenticate the request.
  2. Define request identity. Include the resolved URL with query parameters, method, response type, and representation-changing headers such as language. Account for credential mode and user or tenant identity where applicable; don't use raw access tokens as cache keys. If you can't identify every response-varying input, don't cache the endpoint.
  3. Define freshness. Store an expiry timestamp, evict expired entries, bound memory usage, and invalidate related entries after writes. Respect restrictive server directives; bypass entries requiring revalidation unless you've implemented that validation.
  4. Store real responses. Cache only final HttpResponse events. Replay a response observable, not a serialized event object, and isolate mutable response bodies on both storage and retrieval.
  5. Define scope. Prefer an application-scoped in-memory cache. Clear private entries on identity changes, never share private entries across server-rendered users, and guard against an older in-flight request repopulating an invalidated cache.

The following utility demonstrates only replay mechanics for JSON bodies—not a complete cache interceptor. of emits the cloned response, and structuredClone copies its body because HttpResponse.clone() alone doesn't deep-copy nested data.

import { HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';

export function replayJson<T>(
  response: HttpResponse<T>,
): Observable<HttpResponse<T>> {
  return of(response.clone({ body: structuredClone(response.body) }));
}

Handle Errors, Retries, and Token Refresh Deliberately

Place catchError in the observable pipeline returned from next(req). The logging interceptor above rethrows with throwError(() => error), preserving failure for the caller. Don't turn every failure into an empty successful response; that hides authentication failures and outages.

Retries resubscribe to a failed observable and can send the request again. Use bounded retries with backoff for selected transient failures, and keep interceptors safe to execute repeatedly. Don't blindly retry a POST: the server may already have performed its side effect even if the response was lost; require an endpoint-specific safety contract, such as server-supported idempotency keys.

For token refresh, use a coordinated authentication service: share one in-flight refresh operation among concurrent failures, bypass normal authentication handling for the refresh request, retry each original request at most once with the new token, and propagate refresh failure. A context token can mark bypass or retry state. Refresh belongs in an explicit state machine, not an unbounded recursive catchError branch.

Angular HTTP Interceptor Not Working?

Check Imports and Registration

Use @angular/common/http; the obsolete @angular/http package was removed. Register functions through withInterceptors. For classes, use HTTP_INTERCEPTORS with multi: true and enable them with withInterceptorsFromDi().

There is no general rule requiring interceptor providers to appear before HttpClient. What matters is the configuration and injector used by the client making the request. The class-based example above intentionally places its multi-provider after provideHttpClient.

Check Injector Scope

An injector is Angular's container for resolving dependencies. Configuring another HttpClient in a child injector creates an independent configuration by default; root interceptors won't automatically apply to requests through it. Avoid accidental duplicate configurations. If deliberate child configuration should also use the parent chain, consider withRequestsMadeViaParent(); a parent client must exist.

Check the Observable and Request Path

  • Subscribe. An HttpClient observable doesn't send its request until subscribed. Multiple subscriptions can produce multiple requests.
  • Return the chain. Return next(req) or next.handle(req); don't create an internal subscription and discard the observable.
  • Inspect earlier interceptors. A cache or synthetic response can prevent later handlers from running.
  • Check the client. Direct browser fetch calls aren't requests through Angular's configured interceptor chain.
  • Inspect browser errors. Adding a cross-origin authorization header requires compatible server CORS configuration; an interceptor can't grant that permission.

Test the Configured Chain

Use provideHttpClient(withInterceptors([...])) before provideHttpClientTesting() in tests that configure interceptors, then inspect requests with HttpTestingController. This testing-backend ordering requirement is real and separate from the incorrect general provider-order claim.

Test token changes, unrelated origins, header preservation, response events, and error propagation. For caching, test query-string distinctions, expiry, invalidation, and identity changes—not just a cache hit.

FAQ

How do I add an HTTP interceptor in a standalone Angular app?

Add provideHttpClient(withInterceptors([yourInterceptor])) to the providers in app.config.ts, and pass that configuration to bootstrapApplication. Export your interceptor as an HttpInterceptorFn.

What is the difference between HttpInterceptor and HttpInterceptorFn?

HttpInterceptor is the class interface whose intercept method forwards requests with next.handle(req). HttpInterceptorFn is the functional form and forwards with next(req); Angular recommends functional interceptors for more predictable ordering.

In what order do Angular HTTP interceptors run?

Requests traverse the configured interceptor chain in registration order, while response processing unwinds in reverse order. An interceptor that returns its own response without forwarding can skip the remaining handlers.

How do I add a Bearer token with an Angular interceptor?

Read the current token from your authentication service, validate that the destination is an approved API endpoint, and clone the request with an Authorization header containing Bearer followed by the token. Don't attach credentials to unrelated origins.

Why is my Angular interceptor not being called?

Check that it is registered in the configuration used by the requesting HttpClient, that the request observable is subscribed, and that an earlier interceptor isn't short-circuiting the chain. Class-based interceptors also need HTTP_INTERCEPTORS registration and withInterceptorsFromDi() when using provideHttpClient.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me