In-depth Guides
HTTP Client

Interceptors

HttpClient supports a form of middleware known as interceptors.

TL;DR: Interceptors are middleware that allows common patterns around retrying, caching, logging, and authentication to be abstracted away from individual requests.

HttpClient supports two kinds of interceptors: functional and DI-based. Our recommendation is to use functional interceptors because they have more predictable behavior, especially in complex setups. Our examples in this guide use functional interceptors, and we cover DI-based interceptors in their own section at the end.

Interceptors

Interceptors are generally functions which you can run for each request, and have broad capabilities to affect the contents and overall flow of requests and responses. You can install multiple interceptors, which form an interceptor chain where each interceptor processes the request or response before forwarding it to the next interceptor in the chain.

You can use interceptors to implement a variety of common patterns, such as:

  • Adding authentication headers to outgoing requests to a particular API.
  • Retrying failed requests with exponential backoff.
  • Caching responses for a period of time, or until invalidated by mutations.
  • Customizing the parsing of responses.
  • Measuring server response times and log them.
  • Driving UI elements such as a loading spinner while network operations are in progress.
  • Collecting and batch requests made within a certain timeframe.
  • Automatically failing requests after a configurable deadline or timeout.
  • Regularly polling the server and refreshing results.

Defining an interceptor

The basic form of an interceptor is a function which receives the outgoing HttpRequest and a next function representing the next processing step in the interceptor chain.

For example, this loggingInterceptor will log the outgoing request URL to console.log before forwarding the request:

      
export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {  console.log(req.url);  return next(req);}

In order for this interceptor to actually intercept requests, you must configure HttpClient to use it.

Configuring interceptors

You declare the set of interceptors to use when configuring HttpClient through dependency injection, by using the withInterceptors feature:

      
bootstrapApplication(AppComponent, {providers: [  provideHttpClient(    withInterceptors([loggingInterceptor, cachingInterceptor]),  )]});

The interceptors you configure are chained together in the order that you've listed them in the providers. In the above example, the loggingInterceptor would process the request and then forward it to the cachingInterceptor.

Intercepting response events

An interceptor may transform the Observable stream of HttpEvents returned by next in order to access or manipulate the response. Because this stream includes all response events, inspecting the .type of each event may be necessary in order to identify the final response object.

      
export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {  return next(req).pipe(tap(event => {    if (event.type === HttpEventType.Response) {      console.log(req.url, 'returned a response with status', event.status);    }  }));}

Tip: Interceptors naturally associate responses with their outgoing requests, because they transform the response stream in a closure that captures the request object.

Modifying requests

Most aspects of HttpRequest and HttpResponse instances are immutable, and interceptors cannot directly modify them. Instead, interceptors apply mutations by cloning these objects using the .clone() operation, and specifying which properties should be mutated in the new instance. This might involve performing immutable updates on the value itself (like HttpHeaders or HttpParams).

For example, to add a header to a request:

      
const reqWithHeader = req.clone({  headers: req.headers.set('X-New-Header', 'new header value'),});

This immutability allows most interceptors to be idempotent if the same HttpRequest is submitted to the interceptor chain multiple times. This can happen for a few reasons, including when a request is retried after failure.

CRITICAL: The body of a request or response is not protected from deep mutations. If an interceptor must mutate the body, take care to handle running multiple times on the same request.

Dependency injection in interceptors

Interceptors are run in the injection context of the injector which registered them, and can use Angular's inject API to retrieve dependencies.

For example, suppose an application has a service called AuthService, which creates authentication tokens for outgoing requests. An interceptor can inject and use this service:

      
export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {  // Inject the current `AuthService` and use it to get an authentication token:  const authToken = inject(AuthService).getAuthToken();  // Clone the request to add the authentication header.  const newReq = req.clone({    headers: req.headers.append('X-Authentication-Token', authToken),  });  return next(newReq);}

Request and response metadata

Often it's useful to include information in a request that's not sent to the backend, but is specifically meant for interceptors. HttpRequests have a .context object which stores this kind of metadata as an instance of HttpContext. This object functions as a typed map, with keys of type HttpContextToken.

To illustrate how this system works, let's use metadata to control whether a caching interceptor is enabled for a given request.

Defining context tokens

To store whether the caching interceptor should cache a particular request in that request's .context map, define a new HttpContextToken to act as a key:

      
export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);

The provided function creates the default value for the token for requests that haven't explicitly set a value for it. Using a function ensures that if the token's value is an object or array, each request gets its own instance.

Reading the token in an interceptor

An interceptor can then read the token and choose to apply caching logic or not based on its value:

      
export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {  if (req.context.get(CACHING_ENABLED)) {    // apply caching logic    return ...;  } else {    // caching has been disabled for this request    return next(req);  }}

Setting context tokens when making a request

When making a request via the HttpClient API, you can provide values for HttpContextTokens:

      
const data$ = http.get('/sensitive/data', {  context: new HttpContext().set(CACHING_ENABLED, false),});

Interceptors can read these values from the HttpContext of the request.

The request context is mutable

Unlike other properties of HttpRequests, the associated HttpContext is mutable. If an interceptor changes the context of a request that is later retried, the same interceptor will observe the context mutation when it runs again. This is useful for passing state across multiple retries if needed.

Synthetic responses

Most interceptors will simply invoke the next handler while transforming either the request or the response, but this is not strictly a requirement. This section discusses several of the ways in which an interceptor may incorporate more advanced behavior.

Interceptors are not required to invoke next. They may instead choose to construct responses through some other mechanism, such as from a cache or by sending the request through an alternate mechanism.

Constructing a response is possible using the HttpResponse constructor:

      
const resp = new HttpResponse({  body: 'response body',});

DI-based interceptors

HttpClient also supports interceptors which are defined as injectable classes and configured through the DI system. The capabilities of DI-based interceptors are identical to those of functional interceptors, but the configuration mechanism is different.

A DI-based interceptor is an injectable class which implements the HttpInterceptor interface:

      
@Injectable()export class LoggingInterceptor implements HttpInterceptor {  intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {    console.log('Request URL: ' + req.url);    return handler.handle(req);  }}

DI-based interceptors are configured through a dependency injection multi-provider:

      
bootstrapApplication(AppComponent, {providers: [  provideHttpClient(    // DI-based interceptors must be explicitly enabled.    withInterceptorsFromDi(),  ),  {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true},]});

DI-based interceptors run in the order that their providers are registered. In an app with an extensive and hierarchical DI configuration, this order can be very hard to predict.