Angular HTTP Interceptors Explained — Middleware for HTTP Requests
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
Mutating the original request —
reqis immutable. Always clone withreq.clone()to modify headers, URL, or body.Not handling errors in interceptors — If an interceptor throws, the entire request fails. Always catch errors in error interceptors and re-throw.
Blocking requests in an interceptor — Every interceptor must call
next(req). Forgetting to forward the request hangs the application.Order-dependent interceptor logic — The order of interceptors in the
withInterceptorsarray matters. Auth interceptors should run before cache interceptors.Memory leaks in cache — A cache interceptor that never evicts entries grows indefinitely. Implement TTL and size limits.
Practice Questions
What is an HTTP interceptor? A middleware function that inspects and transforms HTTP requests and responses globally.
How do you modify a request in an interceptor? Clone it with
req.clone()and pass the cloned request tonext().What is the order of interceptor execution? Interceptors run in the order they are provided for requests and reverse order for responses.
How do you handle errors in an interceptor? Use
catchErroroperator on thenext(req)observable and return a handled error.Can an interceptor modify responses? Yes, use
maportapoperators 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
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