Skip to main content

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
Angular provides HttpInterceptorFn for creating functional interceptors. This is the modern, recommended approach.

Basic interceptor

auth.interceptor.ts
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:
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';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, loggingInterceptor])
    )
  ]
};

Common use cases

Authentication token

Add authentication tokens to all requests:
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();

  // 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:
logging.interceptor.ts
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:
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.show();

  return next(req).pipe(
    finalize(() => loadingService.hide())
  );
};
loading.service.ts
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:
cache.interceptor.ts
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:
retry.interceptor.ts
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

auth.interceptor.ts
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

app.config.ts
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:
app.config.ts
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

auth.interceptor.spec.ts
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 interceptorsPrefer HttpInterceptorFn over class-based interceptors for cleaner, more composable code.
Keep interceptors focusedEach 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 logicUse HttpContext to pass metadata to interceptors instead of checking URLs or headers.
Handle errors gracefullyInterceptors 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.