HTTP interceptors are middleware for Angular’s HTTP pipeline. Every HTTP request and response passes through all registered interceptors in order. The auth interceptor adds the JWT Bearer token to every outgoing request — so individual services never need to set auth headers manually. The error interceptor handles API error responses globally — redirecting to login on 401, showing toast notifications on 500, and parsing ValidationProblemDetails from 400 responses. Angular 15+ uses functional interceptors (HttpInterceptorFn) instead of class-based ones.
Functional Interceptors
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn,
HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError, retry } from 'rxjs';
import { Router } from '@angular/router';
// ── Auth Interceptor — adds JWT to every request ───────────────────────────
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.accessToken(); // read from signal
if (!token) return next(req); // no token → pass through unchanged
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(authReq);
};
// ── Error Interceptor — handles API errors globally ────────────────────────
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toastSvc = inject(ToastService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 0:
// Network error (offline, CORS, server down)
toastSvc.error('Network error. Check your connection.');
break;
case 401:
// Unauthorised — redirect to login
router.navigate(['/auth/login'], {
queryParams: { returnUrl: router.url }
});
break;
case 403:
toastSvc.error('You do not have permission for this action.');
break;
case 429:
// Rate limited — parse Retry-After header
const retryAfter = error.headers.get('Retry-After') ?? '60';
toastSvc.warn(`Too many requests. Please wait ${retryAfter}s.`);
break;
case 500:
case 502:
case 503:
toastSvc.error('Server error. Please try again later.');
break;
// 400, 404, 409 etc. — let individual services handle these
}
// Always rethrow so services can handle errors they own
return throwError(() => error);
}),
);
};
// ── Retry Interceptor — retry transient failures ───────────────────────────
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
// Only retry GET requests (idempotent) — never retry POST/PUT/DELETE
if (req.method !== 'GET') return next(req);
return next(req).pipe(
retry({
count: 2,
delay: 1000,
resetOnSuccess: true,
}),
);
};
// ── Logging Interceptor — development-only request logging ────────────────
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const start = Date.now();
console.log(`[HTTP] → ${req.method} ${req.url}`);
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const elapsed = Date.now() - start;
console.log(`[HTTP] ← ${event.status} ${req.url} (${elapsed}ms)`);
}
}),
);
};
// ── Register in app.config.ts ─────────────────────────────────────────────
provideHttpClient(
withInterceptors([
loggingInterceptor, // first in, last out (outermost)
authInterceptor, // adds auth headers
errorInterceptor, // handles response errors
retryInterceptor, // retries failed GETs
])
)
next(req) call passes the (possibly modified) request to the next interceptor in the chain.req.clone() to create a modified copy of the request rather than mutating the original. HttpRequest objects are immutable — attempting to set properties directly throws an error. req.clone({ setHeaders: { Authorization: '...' } }) creates a new request with the additional header while preserving all other request properties (URL, params, method, body). Always clone when modifying a request in an interceptor.throwError(() => error) after handling it globally. If you swallow the error with return EMPTY or return of(null), the service’s error handler and the component’s error state never fire — the observable just completes silently. Services that rely on seeing specific errors (400 validation errors to display form field messages) will miss them if the interceptor swallows them.Common Mistakes
Mistake 1 — Swallowing errors in the interceptor (services miss validation errors)
❌ Wrong — return EMPTY after showing a toast; 400 validation errors never reach the service’s error handler.
✅ Correct — always return throwError(() => error) after logging/toasting; services handle their own errors.
Mistake 2 — Retrying non-idempotent requests (POST/PUT/DELETE retried causes duplicates)
❌ Wrong — retry interceptor retries all failed requests; a failed POST is retried, creating duplicate resources.
✅ Correct — only retry GET requests; never automatically retry POST, PUT, PATCH, or DELETE.