AngularJSHTTP Interceptors

HTTP Interceptors in Angular

HTTP interceptors sit between your application code and the actual HTTP transport. Every outgoing request and every incoming response passes through the interceptor chain, making interceptors the perfect place for cross-cutting concerns like authentication, logging, error handling, and caching.

Since Angular 15, the recommended approach is functional interceptors using HttpInterceptorFn.

How Interceptors Work

The interceptor chain works like middleware:

  1. Request leaves your service
  2. Each interceptor can inspect/modify the request
  3. Request reaches the server
  4. Response comes back through each interceptor in reverse order
  5. Your subscribe callback receives the final response

Use Case

What the Interceptor Does

Auth tokens

Adds Authorization header to every request

Logging

Logs request URL and response time

Error handling

Redirects to login on 401, shows toast on 500

Loading spinner

Increments/decrements a counter per request

Caching

Returns cached response without hitting the server

Request retries

Automatically retries failed requests

Functional Interceptors (Angular 15+)

A functional interceptor is a plain function of type HttpInterceptorFn. Use inject() to access services.

TS
// src/app/interceptors/auth.interceptor.ts
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();

  if (!token) {
    return next(req); // no token — pass request through unchanged
  }

  // Clone the request and add the Authorization header
  const authReq = req.clone({
    headers: req.headers.set('Authorization', `Bearer ${token}`),
  });

  return next(authReq);
};
Note
Requests are immutable. You must clone the request with req.clone() before modifying it — never mutate req directly.
Registering Functional Interceptors

TS
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,    // runs first (outermost)
        loggingInterceptor,
        errorInterceptor,   // runs last before the server
      ])
    ),
  ],
};
Tip
Interceptors are applied in the order they are listed. The first interceptor in the array wraps all subsequent ones.
Logging Interceptor

TS
// src/app/interceptors/logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap, finalize } from 'rxjs/operators';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const start = Date.now();
  console.log(`--> ${req.method} ${req.url}`);

  return next(req).pipe(
    tap({
      next: (event) => {
        // tap into the response stream
      },
      error: (err) => {
        console.error(`<-- ERROR ${req.url}:`, err.status, err.message);
      },
    }),
    finalize(() => {
      const elapsed = Date.now() - start;
      console.log(`<-- ${req.method} ${req.url} [${elapsed}ms]`);
    })
  );
};
Error Handling Interceptor

TS
// src/app/interceptors/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
import { NotificationService } from '../services/notification.service';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const notify = inject(NotificationService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        // Token expired — redirect to login
        router.navigate(['/login']);
      } else if (error.status === 403) {
        notify.error('You do not have permission to do that.');
      } else if (error.status === 404) {
        notify.error('Resource not found.');
      } else if (error.status >= 500) {
        notify.error('Server error. Please try again later.');
      }

      return throwError(() => error);
    })
  );
};
Loading Spinner Interceptor

TS
// src/app/services/loading.service.ts
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoadingService {
  private count = 0;
  readonly isLoading = signal(false);

  start(): void {
    this.count++;
    this.isLoading.set(true);
  }

  stop(): void {
    this.count = Math.max(0, this.count - 1);
    if (this.count === 0) this.isLoading.set(false);
  }
}

// src/app/interceptors/loading.interceptor.ts
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.start();

  return next(req).pipe(
    finalize(() => loadingService.stop())
  );
};
Token Refresh Interceptor

A common pattern is to catch a 401 response, silently refresh the access token, and replay the original request.

TS
// src/app/interceptors/token-refresh.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';

export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status !== 401) {
        return throwError(() => error);
      }

      // Attempt to refresh the token
      return authService.refreshToken().pipe(
        switchMap((newToken) => {
          const retryReq = req.clone({
            headers: req.headers.set('Authorization', `Bearer ${newToken}`),
          });
          return next(retryReq);
        }),
        catchError((refreshError) => {
          authService.logout();
          return throwError(() => refreshError);
        })
      );
    })
  );
};
Warning
Be careful with token refresh interceptors — if the refresh request itself returns 401, you can create an infinite loop. Always exclude the refresh endpoint from the interceptor or add a guard flag.
Caching Interceptor

TS
// src/app/interceptors/cache.interceptor.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';

const cache = new Map<string, HttpResponse<unknown>>();

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  // Only cache GET requests
  if (req.method !== 'GET') {
    return next(req);
  }

  const cached = cache.get(req.url);
  if (cached) {
    console.log('Cache hit:', req.url);
    return of(cached);
  }

  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        cache.set(req.url, event);
      }
    })
  );
};
Class-Based Interceptors (Legacy)

Before Angular 15 functional interceptors, class-based interceptors were the standard. They implement the HttpInterceptor interface.

TS
// src/app/interceptors/auth-class.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthClassInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getToken();
    if (!token) return next.handle(req);

    const authReq = req.clone({
      headers: req.headers.set('Authorization', `Bearer ${token}`),
    });
    return next.handle(authReq);
  }
}

// Registration in AppModule:
// providers: [
//   { provide: HTTP_INTERCEPTORS, useClass: AuthClassInterceptor, multi: true }
// ]
Skipping Interceptors for Specific Requests

TS
import { HttpContext, HttpContextToken } from '@angular/common/http';

// Define a context token
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);

// In your interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.context.get(SKIP_AUTH)) {
    return next(req); // skip this request
  }

  // ... add auth header
  return next(req.clone({ headers: req.headers.set('Authorization', token) }));
};

// Usage — opt out per request
this.http.get('/api/public', {
  context: new HttpContext().set(SKIP_AUTH, true),
})
Best Practices
  • Prefer functional interceptors (HttpInterceptorFn) for new Angular 15+ projects

  • Always clone the request before modifying it — requests are immutable

  • Use HttpContext tokens to opt individual requests out of interceptors

  • Keep each interceptor focused on one concern (single responsibility)

  • Handle errors in a dedicated error interceptor, not scattered across services

  • Use finalize() for cleanup (e.g., stopping a spinner) — it runs on both success and error

  • Guard token refresh interceptors against infinite loops

Summary

HTTP interceptors are one of Angular's most powerful patterns for cross-cutting concerns. Functional interceptors introduced in Angular 15 make them simpler to write, test, and tree-shake. Common use cases include adding auth headers, logging, global error handling, loading spinners, and caching — all without touching individual service methods.