HTTP interceptors form ordered middleware around HttpClient requests. Use them for transport-wide concerns such as authentication headers, timing, correlation, and coordinated loading state while keeping feature-specific business decisions close to the caller.
An interceptor is middleware around HttpClient. It receives an immutable HttpRequest and a next function, returns an Observable of HttpEvent, and can clone a request, transform the event stream, retry, cache, or translate failures.
Prefer functional interceptors for predictable ordering and direct inject usage. Register them explicitly with provideHttpClient(withInterceptors([...])). Keep authentication, tracing, caching, and error policy independently testable.
Clone only when changing a request. Header attachment should check the intended API origin so a credential is not sent to an unrelated URL. Never log authorization headers, session values, or sensitive request bodies.
Attach a bearer token only to the trusted API origins that accept it. Relative URLs are normally same-origin, while an absolute third-party URL must not inherit application credentials merely because it passed through HttpClient.
Cookie-based sessions usually rely on browser cookie rules and XSRF configuration rather than an Authorization header. If expired-token refresh is required, coordinate one refresh request and replay waiting calls only after it succeeds.
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next(authReq);
}
return next(req);
};
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
import { loggingInterceptor } from './logging.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor])
)
]
};
Log operational metadata such as method, route template, duration, status, and a correlation identifier. Redact query values, authorization headers, cookies, request bodies, and response bodies unless a reviewed policy explicitly permits a safe subset.
Use finalize for duration or completion bookkeeping because it runs on success, error, and cancellation. Tap can inspect response events, but logging should not subscribe separately or consume the stream.
import { HttpInterceptorFn } from '@angular/common/http';
import { tap, finalize } from 'rxjs/operators';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const start = Date.now();
console.log(`[HTTP] ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: response => console.log(`[HTTP] Response:`, response),
error: err => console.error(`[HTTP] Error:`, err)
}),
finalize(() => {
const duration = Date.now() - start;
console.log(`[HTTP] ${req.url} completed in ${duration}ms`);
})
);
};
Translate transport details only when callers benefit from a stable application error contract. Preserve status, cause, retryability, and correlation data internally while exposing a safe message or discriminated error kind to the feature.
An interceptor is suitable for cross-cutting policy, not every feature fallback. Returning an empty value globally makes an outage indistinguishable from successful empty data and can let unsafe workflows continue.
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { NotificationService } from './notification.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const notify = inject(NotificationService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
notify.error('You do not have permission.');
break;
case 404:
notify.error('Resource not found.');
break;
case 500:
notify.error('Server error. Please try again later.');
break;
default:
notify.error(`Unexpected error: ${error.message}`);
}
return throwError(() => error);
})
);
};
A global loading indicator must count eligible in-flight requests because calls overlap. Increment before forwarding, decrement in finalize, and expose a derived boolean rather than letting each interceptor invocation set a shared flag directly.
Exclude background polling, prefetching, and requests that already have a local skeleton through an HttpContext token. Add a short display delay when very fast requests would otherwise flash the indicator.
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { LoadingService } from './loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
loading.show();
return next(req).pipe(
finalize(() => loading.hide())
);
};
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoadingService {
private _count = signal(0);
readonly isLoading = this._count.asReadonly();
show() { this._count.update(n => n + 1); }
hide() { this._count.update(n => Math.max(0, n - 1)); }
}
Cache only reads whose response contract permits reuse. A cache key must include every request property that changes the response, including URL, normalized parameters, relevant headers, locale, tenant, and authenticated user scope.
Store immutable response values, define expiration and invalidation, and clear user-specific entries on sign-out. Browser and server caches may already handle HTTP cache headers; an interceptor cache should solve a measured application need rather than duplicate them blindly.
export const CACHE_READ = new HttpContextToken<boolean>(() => false);
export const cacheInterceptor: HttpInterceptorFn = (request, next) => {
if (request.method !== 'GET' || !request.context.get(CACHE_READ)) {
return next(request);
}
const cache = inject(ReadCache);
const key = cache.keyFor(request);
const hit = cache.get(key);
if (hit) return of(hit.clone());
return next(request).pipe(
filter((event): event is HttpResponse<unknown> =>
event instanceof HttpResponse
),
tap(response => cache.put(key, response.clone()))
);
};
this.http.get('/api/catalog', {
context: new HttpContext().set(CACHE_READ, true)
});
Interceptors run in configured order for requests and unwind in reverse for responses. Authentication attachment should not target third-party URLs accidentally, and a refresh interceptor must exclude the refresh call and allow only one refresh operation at a time.
Retry only idempotent operations or writes protected by an API idempotency contract. HttpContext tokens can opt a request into or out of caching, auth, or retry policy without inspecting arbitrary URLs. Preserve cancellation by returning the observable chain rather than creating unmanaged subscriptions.
A token-refresh flow must prevent recursion, exclude the refresh endpoint, coordinate concurrent 401 responses, and fail waiting requests when refresh fails. This is authentication policy, not a generic retry.
Loading indicators need reference counting because requests overlap. Increment when an eligible request starts and decrement in finalize so success, error, and cancellation all release the indicator.
No. HttpRequest and HttpHeaders are immutable.
Concurrent requests finish at different times. The first completion sets a boolean to false even though a second request remains active.
Requests pass through functional interceptors in registration order; responses unwind in the opposite direction. An auth interceptor registered first adds the token before a later logger sees the request. A retry interceptor can cause upstream portions of the chain to execute again depending on where it is placed, which may duplicate logs or loading counters.
Explore 500+ free tutorials across 20+ languages and frameworks.