W3cubDocs

/Angular

HTTP - interceptor use-cases

Following are a number of common uses for interceptors.

Set default headers

Apps often use an interceptor to set default headers on outgoing requests.

The sample app has an AuthService that produces an authorization token. Here is its AuthInterceptor that injects that service to get the token and adds an authorization header with that token to every outgoing request:

import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // Get the auth token from the service.
    const authToken = this.auth.getAuthorizationToken();

    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    const authReq = req.clone({
      headers: req.headers.set('Authorization', authToken)
    });

    // send cloned request with header to the next handler.
    return next.handle(authReq);
  }
}

The practice of cloning a request to set new headers is so common that there's a setHeaders shortcut for it:

// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });

An interceptor that alters headers can be used for a number of different operations, including:

  • Authentication/authorization
  • Caching behavior; for example, If-Modified-Since
  • XSRF protection

Log request and response pairs

Because interceptors can process the request and response together, they can perform tasks such as timing and logging an entire HTTP operation.

Consider the following LoggingInterceptor, which captures the time of the request, the time of the response, and logs the outcome with the elapsed time with the injected MessageService.

import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  constructor(private messenger: MessageService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const started = Date.now();
    let ok: string;

    // extend server response observable with logging
    return next.handle(req)
      .pipe(
        tap({
          // Succeeds when there is a response; ignore other events
          next: (event) => (ok = event instanceof HttpResponse ? 'succeeded' : ''),
          // Operation failed; error is an HttpErrorResponse
          error: (error) => (ok = 'failed')
        }),
        // Log when response observable either completes or errors
        finalize(() => {
          const elapsed = Date.now() - started;
          const msg = `${req.method} "${req.urlWithParams}"
             ${ok} in ${elapsed} ms.`;
          this.messenger.add(msg);
        })
      );
  }
}

The RxJS tap operator captures whether the request succeeded or failed. The RxJS finalize operator is called when the response observable either returns an error or completes and reports the outcome to the MessageService.

Neither tap nor finalize touch the values of the observable stream returned to the caller.

Custom JSON parsing

Interceptors can be used to replace the built-in JSON parsing with a custom implementation.

The CustomJsonInterceptor in the following example demonstrates how to achieve this. If the intercepted request expects a 'json' response, the responseType is changed to 'text' to disable the built-in JSON parsing. Then the response is parsed via the injected JsonParser.

// The JsonParser class acts as a base class for custom parsers and as the DI token.
@Injectable()
export abstract class JsonParser {
  abstract parse(text: string): any;
}

@Injectable()
export class CustomJsonInterceptor implements HttpInterceptor {
  constructor(private jsonParser: JsonParser) {}

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler) {
    if (httpRequest.responseType === 'json') {
      // If the expected response type is JSON then handle it here.
      return this.handleJsonResponse(httpRequest, next);
    } else {
      return next.handle(httpRequest);
    }
  }

  private handleJsonResponse(httpRequest: HttpRequest<any>, next: HttpHandler) {
    // Override the responseType to disable the default JSON parsing.
    httpRequest = httpRequest.clone({responseType: 'text'});
    // Handle the response using the custom parser.
    return next.handle(httpRequest).pipe(map(event => this.parseJsonResponse(event)));
  }

  private parseJsonResponse(event: HttpEvent<any>) {
    if (event instanceof HttpResponse && typeof event.body === 'string') {
      return event.clone({body: this.jsonParser.parse(event.body)});
    } else {
      return event;
    }
  }
}

You can then implement your own custom JsonParser. Here is a custom JsonParser that has a special date reviver.

@Injectable()
export class CustomJsonParser implements JsonParser {
  parse(text: string): any {
    return JSON.parse(text, dateReviver);
  }
}

function dateReviver(key: string, value: any) {
  /* . . . */
}

You provide the CustomParser along with the CustomJsonInterceptor.

{ provide: HTTP_INTERCEPTORS, useClass: CustomJsonInterceptor, multi: true },
{ provide: JsonParser, useClass: CustomJsonParser },

Cache requests

Interceptors can handle requests by themselves, without forwarding to next.handle().

For example, you might decide to cache certain requests and responses to improve performance. You can delegate caching to an interceptor without disturbing your existing data services.

The CachingInterceptor in the following example demonstrates this approach.

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // continue if not cacheable.
    if (!isCacheable(req)) { return next.handle(req); }

    const cachedResponse = this.cache.get(req);
    return cachedResponse ?
      of(cachedResponse) : sendRequest(req, next, this.cache);
  }
}
  • The isCacheable() function determines if the request is cacheable. In this sample, only GET requests to the package search API are cacheable.

  • If the request is not cacheable, the interceptor forwards the request to the next handler in the chain

  • If a cacheable request is found in the cache, the interceptor returns an of() observable with the cached response, by-passing the next handler and all other interceptors downstream

  • If a cacheable request is not in cache, the code calls sendRequest(). This function forwards the request to next.handle() which ultimately calls the server and returns the server's response.

/**
 * Get server response observable by sending request to `next()`.
 * Will add the response to the cache on the way out.
 */
function sendRequest(
  req: HttpRequest<any>,
  next: HttpHandler,
  cache: RequestCache): Observable<HttpEvent<any>> {
  return next.handle(req).pipe(
    tap(event => {
      // There may be other events besides the response.
      if (event instanceof HttpResponse) {
        cache.put(req, event); // Update the cache.
      }
    })
  );
}

Notice how sendRequest() intercepts the response on its way back to the application. This method pipes the response through the tap() operator, whose callback adds the response to the cache.

The original response continues untouched back up through the chain of interceptors to the application caller.

Data services, such as PackageSearchService, are unaware that some of their HttpClient requests actually return cached responses.

Use interceptors to request multiple values

The HttpClient.get() method normally returns an observable that emits a single value, either the data or an error. An interceptor can change this to an observable that emits multiple values.

The following revised version of the CachingInterceptor optionally returns an observable that immediately emits the cached response, sends the request on to the package search API, and emits again later with the updated search results.

// cache-then-refresh
if (req.headers.get('x-refresh')) {
  const results$ = sendRequest(req, next, this.cache);
  return cachedResponse ?
    results$.pipe( startWith(cachedResponse) ) :
    results$;
}
// cache-or-fetch
return cachedResponse ?
  of(cachedResponse) : sendRequest(req, next, this.cache);

The cache-then-refresh option is triggered by the presence of a custom x-refresh header.

A checkbox on the PackageSearchComponent toggles a withRefresh flag, which is one of the arguments to PackageSearchService.search(). That search() method creates the custom x-refresh header and adds it to the request before calling HttpClient.get().

The revised CachingInterceptor sets up a server request whether there's a cached value or not, using the same sendRequest() method described above. The results$ observable makes the request when subscribed.

  • If there's no cached value, the interceptor returns results$.
  • If there is a cached value, the code pipes the cached response onto results$. This produces a recomposed observable that emits two responses, so subscribers will see a sequence of these two responses:
  • The cached response that's emitted immediately
  • The response from the server, that's emitted later
Last reviewed on Tue Nov 08 2022

© 2010–2023 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/guide/http-interceptor-use-cases