Angular HttpClient Interceptor: Centralizing Cross‑Cutting Concerns
Learn how to implement a single HttpInterceptor to handle authentication, error logging, and request transformation, with design considerations, testing steps, and failure modes.
14 Aug 2025, 06:56 UTC

Requirements
Applications often need to apply the same logic to every HTTP request made with HttpClient: attaching an authentication token, logging errors, or transforming request/response payloads. Duplicating this code in each service leads to maintenance overhead and increases the chance of inconsistencies.
Smallest Suitable Design
The Angular HttpInterceptor interface lets you insert a piece of code into the HTTP pipeline. By creating a single class that implements HttpInterceptor and registering it in the root module’s providers array with multi: true, Angular’s dependency‑injection chain will invoke the interceptor for every HttpClient instance.
Trust/Data Boundaries
Treat the interceptor as a trusted boundary:
- For outgoing requests, it can safely add headers (e.g., an auth token) because it runs before the request leaves the application.
- For incoming responses, it can centralize error handling or transformation, keeping service components unaware of these concerns.
Operational Checks
To verify the interceptor works:
- Unit test with
HttpTestingControllerto assert that headers are added and that error responses are handled as expected. - In a running application, open the browser’s network tab and confirm that requests contain the expected headers.
- Optionally, use Angular DevTools or custom logging inside the interceptor to ensure no request bypasses it.
Failure Modes
- If the interceptor throws an error, the observable chain fails and the error propagates to the subscriber.
- Omitting
multi: truewhen providing the interceptor causes Angular to replace rather than append it, effectively ignoring the intended interceptor. - Circular dependency injection (e.g., the interceptor depending on a service that itself uses
HttpClient) can break the injector and cause a runtime error.
Conditions That Would Change the Design
- Need to skip the interceptor for certain endpoints (e.g., public API calls) would require adding a configuration option, such as a custom header or a flag in the request, and checking it inside the interceptor.
- When multiple independent interceptors with strict ordering are required, a factory provider that returns an array of interceptors in the desired sequence may be preferable to relying on implicit order.
Example Implementation
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
// In a real app, retrieve the token from a storage service.
private getToken(): string | null {
return localStorage.getItem('access_token');
}
intercept(
req: HttpRequest,
next: HttpHandler
): Observable> {
const token = this.getToken();
// Clone request and set the Authorization header if a token exists.
let authReq = req;
if (token !== null) {
authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
// Pass the cloned request to the next handler and catch any errors.
return next.handle(authReq).pipe(
catchError((error) => {
// Centralized error handling: log and optionally redirect.
console.error('HTTP error', error);
// Re‑throw so subscribers can still react.
return throwError(() => error);
})
);
}
}
Module Registration
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true, // Important: adds to the chain rather than replacing.
},
],
})
export class CoreModule {}
Verification Steps
- Create a new Angular project:
ng new interceptor-demo. - Generate the interceptor:
ng generate interceptor auth(this creates the class skeleton). - Replace the generated code with the
AuthInterceptorimplementation above. - Provide the interceptor in
AppModuleor a dedicatedCoreModuleas shown. - Run the app:
ng serveand open DevTools → Network. Perform an HTTP call (e.g., via a service) and verify that theAuthorizationheader appears. - For unit testing, add a spec like:
import { TestBed } from '@angular/core/testing';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { AuthInterceptor } from './auth.interceptor';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
describe('AuthInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should add Authorization header when token exists', () => {
localStorage.setItem('access_token', 'test-token');
http.get('/api/data').subscribe();
const req = httpMock.expectOne('/api/data');
expect(req.request.headers.has('Authorization')).toBeTrue();
expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
req.flush({}); // respond with empty body
});
it('should not add Authorization header when token missing', () => {
localStorage.removeItem('access_token');
http.get('/api/data').subscribe();
const req = httpMock.expectOne('/api/data');
expect(req.request.headers.has('Authorization')).toBeFalse();
req.flush({});
});
});
Limitations
- The interceptor runs for all
HttpClientinstances, including those used internally by Angular libraries (e.g., theRouterwhen fetching navigation scripts). If you need to exclude certain calls, add a custom header or flag and check it inside the interceptor. - Because interceptors execute outside the Angular zone, change detection is not triggered automatically after processing a response. If your interceptor updates UI‑relevant state, you may need to run the change detection manually (e.g., inject
ApplicationRefand calltick()) or let the service layer handle UI updates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.