Skip to content

Angular HTTP Interceptors Explained — Middleware for HTTP Requests

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular HTTP Interceptors Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular HTTP interceptors are middleware classes that inspect and transform HTTP requests and responses globally, enabling cross-cutting concerns like authentication, logging, Caching, and error handling.

What You'll Learn

  • How HTTP interceptors work in Angular
  • How to create an interceptor with HttpInterceptorFn
  • How to add authentication headers to requests
  • How to handle HTTP errors globally
  • How to log requests and responses

Why It Matters

Interceptors let you handle cross-cutting concerns in one place instead of duplicating logic across every service. Every request automatically gets auth tokens, every error is handled consistently, and you can add retry logic without touching individual API calls.

Real-World Use

Durga Antivirus Pro uses an auth interceptor that attaches the session token to every API request, a logging interceptor that tracks all API calls for auditing, and an error interceptor that shows user-friendly toast messages and redirects to login on 401 responses.

flowchart LR
    A[HttpClient Request] --> B[Interceptor 1: Auth]
    B --> C[Interceptor 2: Logging]
    C --> D[Interceptor 3: Cache]
    D --> E[API Server]
    E --> F[Interceptor 3: Cache Response]
    F --> G[Interceptor 2: Log Response]
    G --> H[Interceptor 1: Transform]
    H --> I[Component]
    style A fill:#f97316,color:#fff

Creating an Interceptor

Interceptors can be class-based or function-based (functional interceptors are newer and recommended):

import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from "@angular/common/http";
import { Observable } from "rxjs";
import { inject } from "@angular/core";
import { AuthService } from "./auth.service";

export const authInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    const cloned = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next(cloned);
  }

  return next(req);
};

Provision:

import { provideHttpClient, withInterceptors } from "@angular/common/http";
import { authInterceptor } from "./interceptors/auth.interceptor";

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    ),
  ]
}).catch(err => console.error(err));

Expected output: Every HTTP request automatically includes the Authorization: Bearer <token> header when a token exists.

The interceptor function receives the original request and a next function. To modify the request, clone it and apply changes. The cloned request is immutable — the original is never modified.

Error Handling Interceptor

Catch and handle HTTP errors globally:

import { HttpInterceptorFn, HttpErrorResponse } from "@angular/common/http";
import { inject } from "@angular/core";
import { catchError, throwError } from "rxjs";
import { Router } from "@angular/router";
import { ToastService } from "./toast.service";

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const toast = inject(ToastService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      let message = "An unexpected error occurred";

      if (error.status === 0) {
        message = "Network error. Please check your connection.";
      } else if (error.status === 400) {
        message = error.error?.message || "Invalid request.";
      } else if (error.status === 401) {
        message = "Session expired. Please log in again.";
        router.navigate(["/login"]);
      } else if (error.status === 403) {
        message = "You do not have permission to perform this action.";
      } else if (error.status === 404) {
        message = "Resource not found.";
      } else if (error.status >= 500) {
        message = "Server error. Please try again later.";
      }

      toast.show(message, "error");
      console.error("[HTTP Error]", error.status, error.message);
      return throwError(() => error);
    })
  );
};

Expected output: On 401, redirects to login and shows "Session expired". On network errors, shows "Network error". All errors are logged to console.

The error interceptor catches errors from all HTTP calls. It checks the status code and provides appropriate user feedback. The error is re-thrown so individual services can still handle specific errors if needed.

Logging Interceptor

Log all requests and responses for debugging:

import { HttpInterceptorFn, HttpEventType } from "@angular/common/http";
import { tap } from "rxjs/operators";

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const start = performance.now();
  const method = req.method;
  const url = req.urlWithParams;

  console.log(`[HTTP] >> ${method} ${url}`, {
    headers: req.headers.keys(),
    body: req.body
  });

  return next(req).pipe(
    tap(event => {
      if (event.type === HttpEventType.Response) {
        const elapsed = Math.round(performance.now() - start);
        console.log(`[HTTP] << ${method} ${url} (${event.status} in ${elapsed}ms)`, {
          body: event.body
        });
      }
    })
  );
};

Expected output: Console shows outbound requests with method, URL, and headers, followed by response status and timing.

The tap operator lets you inspect the response stream without modifying it. HttpEventType.Response identifies the final response event. The timing calculation measures the full round-trip duration.

Cache Interceptor

Cache GET responses to reduce redundant API calls:

import { HttpInterceptorFn, HttpRequest, HttpEvent, HttpResponse } from "@angular/common/http";
import { of, tap } from "rxjs";
import { Observable } from "rxjs/internal/Observable";

interface CacheEntry {
  url: string;
  response: HttpResponse<unknown>;
  timestamp: number;
}

const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 30000; // 30 seconds

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.method !== "GET") return next(req);

  const cached = cache.get(req.urlWithParams);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    console.log("[Cache] Hit for", req.urlWithParams);
    return of(cached.response.clone());
  }

  return next(req).pipe(
    tap(event => {
      if (event instanceof HttpResponse) {
        console.log("[Cache] Storing", req.urlWithParams);
        cache.set(req.urlWithParams, {
          url: req.urlWithParams,
          response: event.clone(),
          timestamp: Date.now(),
        });
      }
    })
  );
};

Expected output: Repeated GET requests to the same URL within 30 seconds return cached data instead of making a new HTTP call.

The cache stores responses in a Map keyed by the full URL. Before forwarding a GET request, it checks the cache. If a fresh cache entry exists, it returns the cached response as an observable using of.

Request Transformation Interceptor

Transform outgoing requests (e.g., add API prefix, convert format):

import { HttpInterceptorFn } from "@angular/common/http";

export const apiPrefixInterceptor: HttpInterceptorFn = (req, next) => {
  const API_BASE = "https://api.example.com/v2";

  if (!req.url.startsWith("http")) {
    const cloned = req.clone({
      url: `${API_BASE}/${req.url}`,
      setParams: { apiKey: "demo-key" }
    });
    return next(cloned);
  }

  return next(req);
};

export const jsonRequestInterceptor: HttpInterceptorFn = (req, next) => {
  if (!req.headers.has("Content-Type") && req.body && typeof req.body === "object") {
    const cloned = req.clone({
      setHeaders: { "Content-Type": "application/json" }
    });
    return next(cloned);
  }
  return next(req);
};

Expected output: Requests with relative URLs are prefixed with the API base URL, and JSON content type is automatically added for object bodies.

Request transformation interceptors run early in the chain, normalizing requests before they reach other interceptors or the backend.

Common Mistakes

  1. Mutating the original requestreq is immutable. Always clone with req.clone() to modify headers, URL, or body.

  2. Not handling errors in interceptors — If an interceptor throws, the entire request fails. Always catch errors in error interceptors and re-throw.

  3. Blocking requests in an interceptor — Every interceptor must call next(req). Forgetting to forward the request hangs the application.

  4. Order-dependent interceptor logic — The order of interceptors in the withInterceptors array matters. Auth interceptors should run before cache interceptors.

  5. Memory leaks in cache — A cache interceptor that never evicts entries grows indefinitely. Implement TTL and size limits.

Practice Questions

  1. What is an HTTP interceptor? A middleware function that inspects and transforms HTTP requests and responses globally.

  2. How do you modify a request in an interceptor? Clone it with req.clone() and pass the cloned request to next().

  3. What is the order of interceptor execution? Interceptors run in the order they are provided for requests and reverse order for responses.

  4. How do you handle errors in an interceptor? Use catchError operator on the next(req) observable and return a handled error.

  5. Can an interceptor modify responses? Yes, use map or tap operators on the response stream to transform or log responses.

Challenge

Build a RetryInterceptor that automatically retries failed requests up to 3 times with exponential backoff (1s, 2s, 4s delays). Only retry on 5xx server errors and network errors (status 0). Use RxJS retryWhen or retry with delay.

FAQ

What is the difference between HttpInterceptor and HttpInterceptorFn?

HttpInterceptor is the older class-based approach. HttpInterceptorFn is the newer functional approach (Angular 15+).

Can I have multiple interceptors?

Yes, provide them as an array in withInterceptors([...]). They execute in the provided order.

Do interceptors work with fetch API?

Angular's HttpClient uses XMLHttpRequest by default. For fetch API, use withFetch() in provideHttpClient.

Can I conditionally skip an interceptor?

Yes, check the request URL or headers and decide whether to apply the transformation.

How do I test interceptors?

Use HttpTestingController and TestBed to provide mock interceptor and verify requests.

Mini Project

Build an interceptor chain for a todo API client: AuthInterceptor (adds token), LoggingInterceptor (logs request/response timing), ErrorInterceptor (handles 401 redirect, shows toasts), and CacheInterceptor (caches GET responses for 60 seconds). Create a simple TodoService that fetches and creates todos. Verify the interceptor chain works by inspecting console logs and network tab.

What's Next

Continue with route guards and resolvers:

Angular Guards, Angular Resolvers, Angular Lazy Loading

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro