Fix: Angular HTTP Interceptor Not Working — Requests Not Intercepted
Quick Answer
How to fix Angular HTTP interceptors not triggering — provideHttpClient setup, functional interceptors, order of interceptors, excluding specific URLs, and error handling.
The Problem
An Angular HTTP interceptor doesn’t run for outgoing requests:
// Auth interceptor defined — but tokens never attached
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Interceptor running'); // Never logged
const token = localStorage.getItem('token');
const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
return next.handle(authReq);
}
}Or the interceptor runs but error handling doesn’t catch HTTP errors:
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req).pipe(
catchError(err => {
if (err.status === 401) this.authService.logout();
return throwError(() => err);
})
);
}
// 401 errors reach the component — interceptor catchError not triggeredOr with Angular 17+ provideHttpClient, interceptors registered the old way don’t work:
// Registered with HTTP_INTERCEPTORS token — but provideHttpClient is in use
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
// Has no effect when using provideHttpClient()Why This Happens
Angular’s HTTP interceptor system changed significantly in Angular 15+:
HttpClientModulevsprovideHttpClient()— Angular 15+ recommendsprovideHttpClient()inapp.config.ts. The oldHTTP_INTERCEPTORStoken approach only works withHttpClientModule. Mixing the two causes interceptors to be silently ignored.- Interceptor not provided — the interceptor class must be registered in the correct
providersarray. Providing it in a lazy-loaded module instead of the root module means it only applies to requests made within that module’s injector scope. withInterceptorsFromDi()missing — when usingprovideHttpClient(), you must addwithInterceptorsFromDi()to opt into class-based interceptors registered withHTTP_INTERCEPTORS.- Functional interceptors vs class interceptors — Angular 15+ introduced functional interceptors used with
withInterceptors([...]). These are a different API from class-based interceptors and can’t be mixed without the right configuration. - Interceptor order — interceptors run in the order they’re provided. An error-handling interceptor listed after a logging interceptor runs on the response in reverse order.
Fix 1: Register Interceptors Correctly for Your Angular Version
Angular 14 and earlier — HttpClientModule approach:
// app.module.ts
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
@NgModule({
imports: [HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true, // Required — allows multiple interceptors
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true,
},
],
})
export class AppModule {}Angular 15+ — provideHttpClient() with class-based interceptors:
// app.config.ts
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptorsFromDi(), // Enable class-based interceptors
),
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true,
},
],
};Angular 15+ — functional interceptors (preferred):
// auth.interceptor.ts — functional style
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (!token) return next(req);
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(authReq);
};
// app.config.ts — register functional interceptors
import { provideHttpClient, withInterceptors } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor]),
),
],
};Fix 2: Write a Complete Auth Interceptor
A production-ready auth interceptor handles token attachment, refresh, and 401 retry:
// auth.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
// Skip auth header for public endpoints
if (req.url.includes('/auth/login') || req.url.includes('/auth/refresh')) {
return next(req);
}
const token = authService.getAccessToken();
// Attach token if available
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && !req.url.includes('/auth/refresh')) {
// Token expired — try to refresh
return authService.refreshToken().pipe(
switchMap(newToken => {
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${newToken}` },
});
return next(retryReq);
}),
catchError(refreshError => {
// Refresh also failed — log out
authService.logout();
return throwError(() => refreshError);
}),
);
}
return throwError(() => error);
}),
);
};Class-based equivalent:
import { Injectable } from '@angular/core';
import {
HttpInterceptor, HttpRequest, HttpHandler,
HttpEvent, HttpErrorResponse,
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getAccessToken();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next.handle(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return this.authService.refreshToken().pipe(
switchMap(newToken => {
const retried = req.clone({
setHeaders: { Authorization: `Bearer ${newToken}` },
});
return next.handle(retried);
}),
catchError(() => {
this.authService.logout();
return throwError(() => error);
}),
);
}
return throwError(() => error);
}),
);
}
}Fix 3: Set Correct Interceptor Order
Interceptors form a chain. Request modifications apply in registration order (first to last); response modifications apply in reverse (last to first):
// app.config.ts — order matters
provideHttpClient(
withInterceptors([
loggingInterceptor, // 1st — logs request, then logs response last
authInterceptor, // 2nd — adds auth header, handles 401
cacheInterceptor, // 3rd — checks/stores cache, closest to the actual request
]),
)
// Request flow: logging → auth → cache → HTTP request
// Response flow: HTTP response → cache → auth → loggingCommon ordering rules:
- Logging — outermost (first), so it sees the final modified request and the final response
- Auth — before cache, so cached responses don’t bypass auth checks
- Error handling — can be anywhere depending on whether you want to catch errors before or after auth retry
- Retry — after auth, so it retries with the refreshed token
Fix 4: Exclude Specific URLs from Interceptors
Some requests (analytics, third-party APIs, login) shouldn’t have auth headers:
// Functional interceptor — exclude by URL pattern
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const EXCLUDED_URLS = [
'/auth/login',
'/auth/register',
'/public/',
'https://analytics.external.com',
];
const isExcluded = EXCLUDED_URLS.some(url => req.url.includes(url));
if (isExcluded) return next(req);
const token = inject(AuthService).getAccessToken();
if (!token) return next(req);
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
}));
};Use a custom context token to skip interceptors:
import { HttpContext, HttpContextToken } from '@angular/common/http';
// Define a context token
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);
// Interceptor — check the token
export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (req.context.get(SKIP_AUTH)) {
return next(req); // Skip auth for this request
}
// ... attach token
};
// Usage — skip auth for a specific request
this.http.get('/public/health', {
context: new HttpContext().set(SKIP_AUTH, true),
}).subscribe();Fix 5: Write a Logging Interceptor
A logging interceptor that tracks request timing:
// logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap, finalize } from 'rxjs/operators';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = Date.now();
const { method, url } = req;
console.log(`→ ${method} ${url}`);
return next(req).pipe(
tap({
next: (event) => {
// HttpResponse is an HttpEvent — check type
if (event.type === 4) { // HttpEventType.Response = 4
const elapsed = Date.now() - started;
console.log(`← ${method} ${url} ${(event as any).status} (${elapsed}ms)`);
}
},
error: (error) => {
const elapsed = Date.now() - started;
console.error(`✗ ${method} ${url} ${error.status} (${elapsed}ms)`, error.message);
},
}),
finalize(() => {
// Always runs — request completed (success or error)
}),
);
};Fix 6: Handle Errors in Interceptors
A global error handler interceptor:
// error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const notificationService = inject(NotificationService);
const router = inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
// Don't show UI errors for background requests
const isSilent = req.context.get(SILENT_ERROR);
if (!isSilent) {
switch (error.status) {
case 0:
notificationService.error('Network error — check your connection');
break;
case 400:
notificationService.error(error.error?.message ?? 'Bad request');
break;
case 403:
notificationService.error('Access denied');
router.navigate(['/forbidden']);
break;
case 404:
// Don't show notification for 404 — let component handle it
break;
case 500:
notificationService.error('Server error — please try again');
break;
}
}
// Re-throw so components can also handle the error
return throwError(() => error);
}),
);
};Fix 7: Test HTTP Interceptors
Testing interceptors with HttpClientTestingModule:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
describe('AuthInterceptor', () => {
let http: HttpClient;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: AuthService, useValue: { getAccessToken: () => 'test-token' } },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify()); // Ensure no unexpected requests
it('attaches Authorization header', () => {
http.get('/api/data').subscribe();
const req = httpMock.expectOne('/api/data');
expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
req.flush({ data: 'test' });
});
it('redirects to login on 401', () => {
const router = TestBed.inject(Router);
jest.spyOn(router, 'navigate');
http.get('/api/protected').subscribe({ error: () => {} });
const req = httpMock.expectOne('/api/protected');
req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
});Testing functional interceptors:
// app.config.spec.ts — test with provideHttpClient
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
provideHttpClientTesting(),
{ provide: AuthService, useValue: { getAccessToken: () => 'token' } },
],
});Still Not Working?
Lazy-loaded modules and interceptors — interceptors provided in a lazy-loaded module only apply to HttpClient instances injected within that module. For global interceptors, always register in the root AppModule or app.config.ts.
HttpBackend bypasses interceptors — injecting HttpBackend directly (instead of HttpClient) skips all interceptors. Some libraries do this internally. You can’t intercept those requests.
Multiple provideHttpClient() calls — if provideHttpClient() appears in both app.config.ts and a component’s providers, the component gets a separate HttpClient instance without the root-level interceptors.
Interceptor injected in wrong scope — class-based interceptors are instantiated once per injector. If AuthService uses providedIn: 'root' but the interceptor is provided in a feature module, the interceptor gets the root injector’s AuthService — which may have different state than a module-level service.
For related Angular issues, see Fix: Angular RxJS Memory Leak and Fix: Angular Signals Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: RxJS Not Working — Observable Not Emitting, Memory Leak from Unsubscribed Stream, or Operator Behaving Unexpectedly
How to fix RxJS issues — subscription management, switchMap vs mergeMap vs concatMap, error handling with catchError, Subject types, cold vs hot observables, and Angular async pipe.
Fix: Angular Pipe Not Working — Custom Pipe Not Transforming or async Pipe Not Rendering
How to fix Angular pipe issues — declaring pipes in modules, standalone pipe imports, pure vs impure pipes, async pipe with observables, pipe chaining, and custom pipe debugging.
Fix: Angular Signals Not Updating — computed() and effect() Not Triggering
How to fix Angular Signals not updating — signal mutations, computed dependency tracking, effect() cleanup, toSignal() with Observables, and migrating from zone-based change detection.
Fix: Angular Standalone Component Error — Component is Not a Known Element
How to fix Angular standalone component errors — imports array, NgModule migration, RouterModule vs RouterLink, CommonModule replacement, and mixing standalone with module-based components.