Overview
HTTP interceptors provide a mechanism to intercept and modify HTTP requests and responses globally. They act as middleware in the HTTP request pipeline, allowing you to implement cross-cutting concerns like authentication, logging, caching, and error handling.
How interceptors work
Interceptors are called in the order they are provided and form a chain:
Request → Interceptor 1 → Interceptor 2 → Interceptor N → Backend → Server
↓
Response ← Interceptor 1 ← Interceptor 2 ← Interceptor N ← Backend ← Server
Each interceptor can:
Inspect and modify outgoing requests
Inspect and transform incoming responses
Handle errors
Retry requests
Cache responses
Block requests entirely
Functional interceptors (recommended)
Angular provides HttpInterceptorFn for creating functional interceptors. This is the modern, recommended approach.
Basic interceptor
import { HttpInterceptorFn } from '@angular/common/http' ;
export const authInterceptor : HttpInterceptorFn = ( req , next ) => {
// Clone the request and add authorization header
const authReq = req . clone ({
headers: req . headers . set ( 'Authorization' , 'Bearer my-token' )
});
// Pass the cloned request to the next handler
return next ( authReq );
};
Registering functional interceptors
Use withInterceptors() when providing HttpClient:
import { ApplicationConfig } from '@angular/core' ;
import { provideHttpClient , withInterceptors } from '@angular/common/http' ;
import { authInterceptor } from './interceptors/auth.interceptor' ;
import { loggingInterceptor } from './interceptors/logging.interceptor' ;
export const appConfig : ApplicationConfig = {
providers: [
provideHttpClient (
withInterceptors ([ authInterceptor , loggingInterceptor ])
)
]
};
Common use cases
Authentication token
Add authentication tokens to all requests:
import { HttpInterceptorFn } from '@angular/common/http' ;
import { inject } from '@angular/core' ;
import { AuthService } from '../services/auth.service' ;
export const authInterceptor : HttpInterceptorFn = ( req , next ) => {
const authService = inject ( AuthService );
const token = authService . getToken ();
// Skip auth for login/register endpoints
if ( req . url . includes ( '/auth/' )) {
return next ( req );
}
// Add token if available
if ( token ) {
const authReq = req . clone ({
headers: req . headers . set ( 'Authorization' , `Bearer ${ token } ` )
});
return next ( authReq );
}
return next ( req );
};
Logging
Log all HTTP requests and responses:
import { HttpInterceptorFn , HttpResponse } from '@angular/common/http' ;
import { tap } from 'rxjs/operators' ;
export const loggingInterceptor : HttpInterceptorFn = ( req , next ) => {
const started = Date . now ();
console . log ( `HTTP ${ req . method } ${ req . url } - Started` );
return next ( req ). pipe (
tap ({
next : ( event ) => {
if ( event instanceof HttpResponse ) {
const elapsed = Date . now () - started ;
console . log ( `HTTP ${ req . method } ${ req . url } - Success ( ${ elapsed } ms)` );
console . log ( 'Response:' , event . body );
}
},
error : ( error ) => {
const elapsed = Date . now () - started ;
console . error ( `HTTP ${ req . method } ${ req . url } - Failed ( ${ elapsed } ms)` );
console . error ( 'Error:' , error );
}
})
);
};
Loading indicator
Show a loading spinner during HTTP requests:
import { HttpInterceptorFn } from '@angular/common/http' ;
import { inject } from '@angular/core' ;
import { finalize } from 'rxjs/operators' ;
import { LoadingService } from '../services/loading.service' ;
export const loadingInterceptor : HttpInterceptorFn = ( req , next ) => {
const loadingService = inject ( LoadingService );
loadingService . show ();
return next ( req ). pipe (
finalize (() => loadingService . hide ())
);
};
import { Injectable } from '@angular/core' ;
import { BehaviorSubject } from 'rxjs' ;
@ Injectable ({ providedIn: 'root' })
export class LoadingService {
private loadingSubject = new BehaviorSubject < boolean >( false );
loading$ = this . loadingSubject . asObservable ();
show () {
this . loadingSubject . next ( true );
}
hide () {
this . loadingSubject . next ( false );
}
}
Caching
Cache GET requests:
import { HttpInterceptorFn , HttpResponse } from '@angular/common/http' ;
import { inject } from '@angular/core' ;
import { of } from 'rxjs' ;
import { tap } from 'rxjs/operators' ;
import { CacheService } from '../services/cache.service' ;
export const cacheInterceptor : HttpInterceptorFn = ( req , next ) => {
// Only cache GET requests
if ( req . method !== 'GET' ) {
return next ( req );
}
const cacheService = inject ( CacheService );
const cachedResponse = cacheService . get ( req . url );
// Return cached response if available
if ( cachedResponse ) {
console . log ( `Returning cached response for ${ req . url } ` );
return of ( cachedResponse );
}
// Otherwise, make the request and cache the response
return next ( req ). pipe (
tap ( event => {
if ( event instanceof HttpResponse ) {
cacheService . set ( req . url , event );
}
})
);
};
API prefix
Add base URL to all requests:
api-prefix.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http' ;
import { environment } from '../environments/environment' ;
export const apiPrefixInterceptor : HttpInterceptorFn = ( req , next ) => {
// Skip if already absolute URL
if ( req . url . startsWith ( 'http://' ) || req . url . startsWith ( 'https://' )) {
return next ( req );
}
// Add API base URL
const apiReq = req . clone ({
url: ` ${ environment . apiUrl }${ req . url } `
});
return next ( apiReq );
};
Retry logic
Automatically retry failed requests:
import { HttpInterceptorFn , HttpErrorResponse } from '@angular/common/http' ;
import { retry , timer } from 'rxjs' ;
import { catchError } from 'rxjs/operators' ;
export const retryInterceptor : HttpInterceptorFn = ( req , next ) => {
return next ( req ). pipe (
retry ({
count: 3 ,
delay : ( error , retryCount ) => {
// Only retry on server errors (5xx)
if ( error instanceof HttpErrorResponse && error . status >= 500 ) {
console . log ( `Retry attempt ${ retryCount } for ${ req . url } ` );
// Exponential backoff: 1s, 2s, 4s
return timer ( 1000 * Math . pow ( 2 , retryCount - 1 ));
}
// Don't retry for client errors
throw error ;
}
})
);
};
Class-based interceptors (legacy)
The older class-based approach using HttpInterceptor interface is still supported but not recommended for new code.
Creating a class-based interceptor
import { Injectable } from '@angular/core' ;
import {
HttpInterceptor ,
HttpRequest ,
HttpHandler ,
HttpEvent
} from '@angular/common/http' ;
import { Observable } from 'rxjs' ;
@ Injectable ()
export class AuthInterceptor implements HttpInterceptor {
intercept ( req : HttpRequest < any >, next : HttpHandler ) : Observable < HttpEvent < any >> {
const authReq = req . clone ({
headers: req . headers . set ( 'Authorization' , 'Bearer my-token' )
});
return next . handle ( authReq );
}
}
Registering class-based interceptors
import { ApplicationConfig } from '@angular/core' ;
import { provideHttpClient , withInterceptorsFromDi } from '@angular/common/http' ;
import { HTTP_INTERCEPTORS } from '@angular/common/http' ;
import { AuthInterceptor } from './interceptors/auth.interceptor' ;
export const appConfig : ApplicationConfig = {
providers: [
provideHttpClient ( withInterceptorsFromDi ()),
{
provide: HTTP_INTERCEPTORS ,
useClass: AuthInterceptor ,
multi: true
}
]
};
Modifying requests
Requests are immutable, so you must clone them to make changes:
export const modifyRequestInterceptor : HttpInterceptorFn = ( req , next ) => {
// Clone and modify URL
const modifiedReq = req . clone ({
url: req . url . replace ( '/api/' , '/api/v2/' )
});
// Clone and add headers
const withHeaders = req . clone ({
headers: req . headers
. set ( 'X-Custom-Header' , 'value' )
. set ( 'Accept' , 'application/json' )
});
// Clone and add query parameters
const withParams = req . clone ({
params: req . params
. set ( 'timestamp' , Date . now (). toString ())
. set ( 'version' , '2.0' )
});
// Clone and modify body
const withBody = req . clone ({
body: { ... req . body , timestamp: Date . now () }
});
return next ( withHeaders );
};
Modifying responses
Transform response data:
transform-response.interceptor.ts
import { HttpInterceptorFn , HttpResponse } from '@angular/common/http' ;
import { map } from 'rxjs/operators' ;
export const transformResponseInterceptor : HttpInterceptorFn = ( req , next ) => {
return next ( req ). pipe (
map ( event => {
if ( event instanceof HttpResponse ) {
// Transform the response body
const transformedBody = {
data: event . body ,
timestamp: Date . now (),
url: req . url
};
// Return a new response with transformed body
return event . clone ({
body: transformedBody
});
}
return event ;
})
);
};
Conditional interception
Apply interceptor logic conditionally:
conditional.interceptor.ts
import { HttpInterceptorFn , HttpContext , HttpContextToken } from '@angular/common/http' ;
// Create a context token
export const SKIP_AUTH = new HttpContextToken < boolean >(() => false );
export const conditionalAuthInterceptor : HttpInterceptorFn = ( req , next ) => {
// Check context
if ( req . context . get ( SKIP_AUTH )) {
return next ( req );
}
// Apply auth logic
const authReq = req . clone ({
headers: req . headers . set ( 'Authorization' , 'Bearer token' )
});
return next ( authReq );
};
Use context when making requests:
import { HttpContext } from '@angular/common/http' ;
import { SKIP_AUTH } from './interceptors/conditional.interceptor' ;
// Skip authentication for this request
http . get ( '/public/data' , {
context: new HttpContext (). set ( SKIP_AUTH , true )
}). subscribe ();
Interceptor order
Interceptors execute in the order they are provided:
provideHttpClient (
withInterceptors ([
apiPrefixInterceptor , // 1. Add base URL
authInterceptor , // 2. Add auth token
loggingInterceptor , // 3. Log request
cacheInterceptor , // 4. Check cache
retryInterceptor // 5. Retry on failure
])
)
On the response path, they execute in reverse order:
Request: apiPrefix → auth → logging → cache → retry → backend
Response: backend → retry → cache → logging → auth → apiPrefix
Accessing dependencies
Use inject() to access services in functional interceptors:
import { HttpInterceptorFn } from '@angular/common/http' ;
import { inject } from '@angular/core' ;
import { AuthService } from '../services/auth.service' ;
import { Router } from '@angular/router' ;
export const authInterceptor : HttpInterceptorFn = ( req , next ) => {
const authService = inject ( AuthService );
const router = inject ( Router );
const token = authService . getToken ();
if ( ! token && ! req . url . includes ( '/public' )) {
router . navigate ([ '/login' ]);
throw new Error ( 'Not authenticated' );
}
// Continue with request
return next ( req );
};
Testing interceptors
import { TestBed } from '@angular/core/testing' ;
import { HttpTestingController , provideHttpClientTesting } from '@angular/common/http/testing' ;
import { HttpClient , provideHttpClient , withInterceptors } from '@angular/common/http' ;
import { authInterceptor } from './auth.interceptor' ;
describe ( 'AuthInterceptor' , () => {
let httpClient : HttpClient ;
let httpMock : HttpTestingController ;
beforeEach (() => {
TestBed . configureTestingModule ({
providers: [
provideHttpClient ( withInterceptors ([ authInterceptor ])),
provideHttpClientTesting ()
]
});
httpClient = TestBed . inject ( HttpClient );
httpMock = TestBed . inject ( HttpTestingController );
});
afterEach (() => {
httpMock . verify ();
});
it ( 'should add Authorization header' , () => {
httpClient . get ( '/api/users' ). subscribe ();
const req = httpMock . expectOne ( '/api/users' );
expect ( req . request . headers . has ( 'Authorization' )). toBe ( true );
expect ( req . request . headers . get ( 'Authorization' )). toContain ( 'Bearer' );
req . flush ([]);
});
});
Best practices
Use functional interceptors Prefer HttpInterceptorFn over class-based interceptors for cleaner, more composable code.
Keep interceptors focused Each interceptor should have a single responsibility. Create multiple small interceptors instead of one large one.
Always clone requests before modifying them. Requests are immutable by design.
Use context for conditional logic Use HttpContext to pass metadata to interceptors instead of checking URLs or headers.
Handle errors gracefully Interceptors should not crash. Always handle errors and decide whether to propagate them or recover.
Next steps
Error handling Learn how to handle HTTP errors effectively.
HttpClient Back to HttpClient documentation.