Skip to content

Fix: Angular HTTP Interceptor Not Working — Requests Not Intercepted

FixDevs ·

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 triggered

Or 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+:

  • HttpClientModule vs provideHttpClient() — Angular 15+ recommends provideHttpClient() in app.config.ts. The old HTTP_INTERCEPTORS token approach only works with HttpClientModule. Mixing the two causes interceptors to be silently ignored.
  • Interceptor not provided — the interceptor class must be registered in the correct providers array. 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 using provideHttpClient(), you must add withInterceptorsFromDi() to opt into class-based interceptors registered with HTTP_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 → logging

Common 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles