82

In the documentation about the new HttpClientModule included in the new version of Angular 4.3, the mechanism to intercept requests is explained very well. There is also mention of the response interceptor mechanism however I cannot find anything about it.

Does anyone have an idea about how to intercept a response in order to modify the body message before it is sent to the service?

Thanks.

astro8891
  • 502
  • 2
  • 7
  • 18
olivier houssin
  • 842
  • 1
  • 6
  • 11

4 Answers4

65

I recently made an HttpInterceptor in order to resolve cyclical references in some JSON on the client side, essentially replacing any object with a $ref property with the object in the JSON that has a matching $id property. (This is the output you get if Json.Net is configured with PreserveReferencesHandling.Objects and ReferenceLoopHandling.Ignore).

The answers here helped me some of way, but none of them show how to modify the body of the response, like the OP needs. In order to do so, one needs to clone the event and update the body, like so:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).map(event => {
        if (event instanceof HttpResponse && shouldBeIntercepted(event)) {
            event = event.clone({ body: resolveReferences(event.body) })
        }         
        return event;
    });
}

Any event that should not be modified is simply passed through to the next handler.

lealceldeiro
  • 14,342
  • 6
  • 49
  • 80
sigbjornlo
  • 1,033
  • 8
  • 20
  • 3
    ^^ The only answer that actually shows how to modify response, worked for me in Angular 5.2.4 typescript 2.6.2 – mikhail-t Feb 26 '18 at 21:32
  • I was looking for this too, and now I realize it is the same as changing the request: `request = request.clone({ url: 'new-url' })`. Very useful – Guillaume Feb 27 '18 at 10:28
  • 22
    If you came here in 2019 - pipe also needed: `import { map } from 'rxjs/operators';` `next.handle(clonedRequest).pipe(map(event => { ... }))` – Dmitry Gusarov Mar 25 '19 at 23:10
  • Works perfectly with @DmitryGusarov's change. If specifically trying to solve the same problem with JSON.net $ref/$id, I used this for resolving the references: https://stackoverflow.com/questions/21686499/how-to-restore-circular-references-e-g-id-from-json-net-serialized-json – Jeppe Jul 20 '19 at 14:55
  • but what if i wish to interrupt the interceptor chain if in response there is something wrong? – Andrea Scarafoni Mar 02 '21 at 16:05
  • Is it necessary to clone and return event? Can we directly send the event.body? – kunj choksi Mar 15 '22 at 20:48
59

I suppose you can use do as @federico-scamuzzi suggested, or you can use map and catch like so:

import { Injectable } from '@angular/core';
import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
  HttpResponse
} from '@angular/common/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.info('req.headers =', req.headers, ';');
    return next.handle(req)
      .map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse && ~~(event.status / 100) > 3) {
          console.info('HttpResponse::event =', event, ';');
        } else console.info('event =', event, ';');
        return event;
      })
      .catch((err: any, caught) => {
        if (err instanceof HttpErrorResponse) {
          if (err.status === 403) {
            console.info('err.error =', err.error, ';');
          }
          return Observable.throw(err);
        }
      });
  }
}

EDIT: @LalitKushwah was asking about redirecting if(!loggedIn). I use Route Guards, specifically:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot
       } from '@angular/router';

import { Observable } from 'rxjs/Observable';

import { AuthService } from '../../api/auth/auth.service';
import { AlertsService } from '../alerts/alerts.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private router: Router,
              private alertsService: AlertsService) {}

  canActivate(next: ActivatedRouteSnapshot,
              state: RouterStateSnapshot
              ): Observable<boolean> | Promise<boolean> | boolean {
    if (AuthService.loggedIn()) return true;

    const url: string = state.url;

    this.alertsService.add(`Auth required to view ${url}`);
    this.router
      .navigate(['/auth'], { queryParams: { redirectUrl: url } })
      .then(() => {});
    return false;
  }
}

Then I can simply add that as an argument to my route:

{
  path: 'dashboard', loadChildren:'app/dashboard/dashboard.module#DashboardModule',
  canActivate: [AuthGuard]
}

EDIT: As of Angular 15 released Nov 2022 you can use this functional pattern:

const route = {
  path: 'admin',
  canActivate: [() => inject(LoginService).isLoggedIn()]
};
A T
  • 13,008
  • 21
  • 97
  • 158
  • 3
    you're my hero. hope one day you'll receive an oscar! – Tim Kretschmer Oct 27 '17 at 09:09
  • 2
    Can you please explain `~(event.status / 100) > 3`? – sabithpocker Oct 30 '17 at 12:54
  • If it's a 400 or 500 HTTP error then that condition will be true. `~` deals with str->num conversion—in this context—better than `+`. – A T Oct 30 '17 at 21:05
  • Can we use NavController in interceptor while intercepting response? The scenario is if I got 'loggedIn false' in response so I want to redirect user to Login page. – Lalit Kushwah Jan 23 '18 at 11:27
  • @LalitKushwah I have updated my answer to resolve your query. – A T Jan 23 '18 at 12:05
  • @RoyiNamir: Whoops, missing my second `~` there. Good find. Edited. – A T Feb 22 '18 at 15:12
  • Do you provide this injectable as an service inside your app module? – Michael W. Czechowski Apr 12 '18 at 12:35
  • But isn't HttpResponse status property already a number? At least it seems so with angular 5.2.2 – Rui Marques May 22 '18 at 12:31
  • [`HttpResponse['status']`](https://angular.io/api/common/http/HttpResponseBase#status) is indeed a `number`. But dividing it by `100` could leave a decimal, I want whole non-negative numbers (`unsigned int` in C). Because I want a `301` response code to be truthy. `3.01` is greater than `3`. – A T Jun 03 '18 at 12:27
  • Small remark: to get only responses **with a body**, instead of checking for `instanceof` you may use `event.type === HttpEventType.Response` (enum is exposed in `@angular/common/http` module). – dbardakov Oct 17 '18 at 16:01
  • 3
    Since event.status is already number would this not suffice: if(event.status >= 400)? It feel like that's much easier to read and does the same thing. – tkd_aj Nov 16 '18 at 17:30
  • One question. How can i get data from a promise and append it to req.clone? – shekhar chander Dec 27 '22 at 17:19
47

Since Angular 6 release, RxJs 6.0 changed its interface, so you cannot use operators the same way (like .map(), .tap()...).
Because of that, most of the above solutions are outdated.
This is how you correctly modify content of an Observable using RxJs 6.0+ (with pipe):


import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Injectable()
export class ResponseInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> {

        return next.handle(req).pipe(map((event: HttpEvent<any>) => {
            if (event instanceof HttpResponse) {
                event = event.clone({body: this.modifyBody(event.body)});
            }
            return event;
        }));

    }

    private modifyBody(body: any) {
        /*
        * write your logic to modify the body
        * */
    }
}

Random
  • 3,158
  • 1
  • 15
  • 25
Imal Hasaranga Perera
  • 9,683
  • 3
  • 51
  • 41
6

From what i can understand (I've only done the intercept for request and inject auth token) .. you can attach a .do() and test if is a reponse .. like (as doc says):

import 'rxjs/add/operator/do';

export class TimingInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const started = Date.now();
    return next
      .handle(req)
      .do(event => {
        if (event instanceof HttpResponse) { //<-- HERE
          const elapsed = Date.now() - started;
          console.log(event} ms.`);
        }
      });
  }

}
sabithpocker
  • 15,274
  • 1
  • 42
  • 75
federico scamuzzi
  • 3,708
  • 1
  • 17
  • 24