Angular HTTP Interceptors
In Angular applications, HTTP interceptors provide a powerful mechanism to intercept and modify HTTP requests and responses. They are essential tools for implementing cross-cutting concerns like authentication, logging, error handling, and request transformation in a centralized way.
What are HTTP Interceptors?
HTTP Interceptors act as middleware for HTTP requests and responses in Angular applications. They allow you to:
- Add authentication tokens to outgoing requests
- Log HTTP activity
- Handle errors uniformly across your application
- Transform request and response data
- Show loading indicators during HTTP operations
- Implement caching strategies
Interceptors work by tapping into the Angular HTTP client's request pipeline, enabling you to apply consistent behavior to all HTTP communication without having to repeat code in multiple services.
How Do Interceptors Work?
Interceptors implement the HttpInterceptor
interface from @angular/common/http
, which requires a single method called intercept()
. This method receives the current request and the next handler in the chain.
The interceptor can then:
- Examine the request
- Modify it if needed
- Pass it to the next handler
- Catch and process the response or error before returning it to the caller
Creating Your First Interceptor
Let's start by creating a basic HTTP interceptor that adds an authentication token to each request:
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Get token from localStorage
const token = localStorage.getItem('auth_token');
if (token) {
// Clone the request and add the token
const authRequest = request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`)
});
// Pass the modified request to the next handler
return next.handle(authRequest);
}
// Pass the original request if no token exists
return next.handle(request);
}
}
Important Points:
- Requests are immutable, so we need to clone them to make modifications
next.handle()
returns an Observable that we must return from ourintercept
method- Interceptors can be chained, with each one calling the next in sequence
Registering an Interceptor
To use your interceptor, you need to register it in your application's module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { AuthInterceptor } from './interceptors/auth.interceptor';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true // This is important as it allows multiple interceptors
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
The multi: true
option is crucial as it tells Angular to add this interceptor to the collection of interceptors rather than replacing any existing ones.
Common Use Cases for Interceptors
1. Logging HTTP Requests
A logging interceptor can help during development and debugging:
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
console.log(`Request sent to ${request.url}`);
return next.handle(request).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.log(`Response received from ${request.url}`, event);
}
})
);
}
}
2. Error Handling
Centralized error handling:
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private notificationService: NotificationService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
let errorMsg = '';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMsg = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMsg = `Error Code: ${error.status}, Message: ${error.message}`;
// Handle specific status codes
if (error.status === 401) {
// Redirect to login page or refresh token
this.notificationService.show('Your session has expired. Please login again.');
}
}
this.notificationService.show(errorMsg);
return throwError(() => new Error(errorMsg));
})
);
}
}
3. Loading Indicator
Show loading indicators during HTTP requests:
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
this.loadingService.show();
return next.handle(request).pipe(
finalize(() => {
this.loadingService.hide();
})
);
}
}
4. Caching Responses
A simple caching implementation:
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
private cache: Map<string, HttpResponse<any>> = new Map();
constructor() {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Only cache GET requests
if (request.method !== 'GET') {
return next.handle(request);
}
// Check if we have a cached response
const cachedResponse = this.cache.get(request.url);
if (cachedResponse) {
return of(cachedResponse.clone());
}
// Send request to server and cache response
return next.handle(request).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(request.url, event.clone());
}
})
);
}
}
Real-world Example: Authentication and Token Refresh
Here's a more advanced example that handles token refreshing when an API returns a 401 Unauthorized response:
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService, private router: Router) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add token to request
let authReq = this.addTokenToRequest(request);
// Handle the request and catch potential errors
return next.handle(authReq).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
// Try to refresh token if unauthorized
return this.handle401Error(request, next);
}
return throwError(() => error);
})
);
}
private addTokenToRequest(request: HttpRequest<any>): HttpRequest<any> {
const token = this.authService.getToken();
if (token) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return request;
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
// Retry the request with new token
return next.handle(this.addTokenToRequest(request));
}),
catchError(err => {
this.isRefreshing = false;
this.authService.logout();
this.router.navigate(['/login']);
return throwError(() => err);
})
);
} else {
// Wait until refreshToken is completed
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap(() => next.handle(this.addTokenToRequest(request)))
);
}
}
}
This interceptor:
- Adds the authentication token to all requests
- Catches 401 errors and attempts to refresh the token
- Queues pending requests while refreshing the token
- Retries failed requests with the new token
- Logs the user out if token refresh fails
Best Practices for Using Interceptors
- Keep them focused: Each interceptor should have a single responsibility
- Order matters: Interceptors run in the order they're provided
- Be mindful of performance: Unnecessary work in interceptors affects all HTTP requests
- Handle errors properly: Always catch and handle errors to prevent breaking the request chain
- Don't modify request body: Focus on headers and metadata, not the request payload
- Remember immutability: Always clone requests before modifying them
- Avoid side effects: Don't make UI changes directly from interceptors
Summary
HTTP interceptors provide a powerful way to centralize cross-cutting concerns in Angular applications. They allow you to:
- Modify HTTP requests and responses
- Implement authentication and authorization
- Handle errors consistently
- Log HTTP traffic
- Implement caching and loading indicators
By using interceptors effectively, you can keep your component and service code clean while ensuring consistent behavior across your application's HTTP communication.
Additional Resources
- Angular Official Documentation on HTTP Interceptors
- RxJS Documentation
- Understanding Observables in Angular
Exercises
- Create a timing interceptor that logs how long each HTTP request takes
- Implement a retry interceptor that automatically retries failed requests up to 3 times
- Create an interceptor that adds custom headers for specific API endpoints only
- Build a comprehensive authentication system using interceptors for token management
- Implement a response transformation interceptor that handles pagination metadata
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)