71

How to cancel/abort all pending HTTP requests in angular 4+.

There is an unsubscribe method to cancel HTTP Requests but how to cancel all pending requests all at once.

Especially while route change.

There is one thing I did

ngOnDestroy() {
  this.subscription.unsubscribe();
}

but how to achieve this globally

Any Ideas?

Srdjan Pazin
  • 103
  • 2
  • 5
Sibiraj
  • 4,486
  • 7
  • 33
  • 57

11 Answers11

96

Checkout the takeUntil() operator from RxJS to globally drop your subscriptions :

- RxJS 6+ (using the pipe syntax)

import { takeUntil } from 'rxjs/operators';

export class YourComponent {
   protected ngUnsubscribe: Subject<void> = new Subject<void>();

   [...]

   public httpGet(): void {
      this.http.get()
          .pipe( takeUntil(this.ngUnsubscribe) )
          .subscribe( (data) => { ... });
   }

   public ngOnDestroy(): void {
       // This aborts all HTTP requests.
       this.ngUnsubscribe.next();
       // This completes the subject properlly.
       this.ngUnsubscribe.complete();
   }
}

- RxJS < 6

import 'rxjs/add/operator/takeUntil'

export class YourComponent {
   protected ngUnsubscribe: Subject<void> = new Subject<void>();

   [...]

   public httpGet(): void {
      this.http.get()
         .takeUntil(this.ngUnsubscribe)
         .subscribe( (data) => { ... })
   }

   public ngOnDestroy(): void {
       this.ngUnsubscribe.next();
       this.ngUnsubscribe.complete();
   }
}

You can basically emit an event on your unsubscribe Subject using next() everytime you want to complete a bunch of streams. It is also good practice to unsubscribe to active Observables as the component is destroyed, to avoid memory leaks.

Worth reading :

Alexis Facques
  • 1,783
  • 11
  • 19
73

You can create an interceptor to apply takeUntil operator to every request. Then on route change you will emit event that will cancel all pending requests.

@Injectable()
export class HttpCancelInterceptor implements HttpInterceptor {
  constructor(private httpCancelService: HttpCancelService) { }

  intercept<T>(req: HttpRequest<T>, next: HttpHandler): Observable<HttpEvent<T>> {
    return next.handle(req).pipe(takeUntil(this.httpCancelService.onCancelPendingRequests()))
  }
}

Helper service.

@Injectable()
export class HttpCancelService {
  private cancelPendingRequests$ = new Subject<void>()

  constructor() { }

  /** Cancels all pending Http requests. */
  public cancelPendingRequests() {
    this.cancelPendingRequests$.next()
  }

  public onCancelPendingRequests() {
    return this.cancelPendingRequests$.asObservable()
  }

}

Hook on route changes somewhere in your app (e.g. onInit in appComponent).

this.router.events.subscribe(event => {
  if (event instanceof ActivationEnd) {
    this.httpCancelService.cancelPendingRequests()
  }
})

And last but not least, register the interceptor to your app.module.ts:

  import { HttpCancelInterceptor } from 'path/to/http-cancel.interceptor';
  import { HTTP_INTERCEPTORS } from '@angular/common/http';

  @NgModule({
    [...]
    providers: [
      {
        multi: true,
        provide: HTTP_INTERCEPTORS,
        useClass: HttpCancelInterceptor
      }
    ],
    [...]
  })
  export class AppModule { }
XDS
  • 3,786
  • 2
  • 36
  • 56
Bladito
  • 1,079
  • 11
  • 10
  • 8
    I think this is the best way – walidtlili May 10 '18 at 13:46
  • 2
    this seems great but it's forgetting to complete the subject to close it – Logus Graphics Sep 14 '18 at 20:44
  • This approach is very useful to cancel every pending http request when route changes. I guess, to cancel other subscription(s), we still need to cancel those request separately. – hbthanki Dec 23 '18 at 00:30
  • 1
    @hbthanki Yes you need to cancel other requests manually. As unsubscribing everything can be annoying, I usually have a class that implements onDestroy and my components extend this class (I call it Destroyable). It has a public Subject which emits and completes on destroy. My components then have a takeUntil(this.destroyed$) on every observable. So this approach cancels all pending observables when you destroy the component. – Bladito Jan 14 '19 at 15:34
  • 2
    @Logus I didn't close it deliberately because the service lives through the entire time of the app existence and closing the stream would not free any resources anyway. If the subject completed then you would need to create the new one over and over. And when would it create? Who would have this responsibility? It would just make the code more complicated and I'm afraid it would add no value. Feel free to correct me if I'm wrong. – Bladito Jan 14 '19 at 15:44
  • 1
    Thank you for the useful answer. I just wanted to add 'next.handle(req).takeUntil()' won't work.(It didn't worked for me using angular 7 and rxjs version 6.3.3). I used next.handle(req).pipe(takeUntil()) instead. – Adnan Sheikh Oct 02 '19 at 08:11
  • @AdnanSheikh yes you're right. When I was writing the answer, the current rxjs did not have pipes yet. Operators were just chainable after each other. In the newest version it is written with pipe (just as you mentioned). Maybe I should update the answer to reflect new rxjs api. Thanks for your comment. – Bladito Oct 02 '19 at 12:17
  • How to handle the case with duplicate HTTP request? I have an endpoint to upload large file. I want to cancel the pending upload request if new request is created before earlier one is completed. – Anuj TBE Dec 13 '19 at 10:44
  • @AnujTBE well if you wanted to use the solution above, then you would just call cancelPendingRequests() right before you call the upload (so you kill any existing requests) - but that could also kill other requests you may want to keep. Or you could modify the code and add parameter (e.g. url string) to the cancelPendingRequests method and then filter the observable inside onCancelPendingRequests method, so only the request with that url would get interrupted. But I suggest you look into switchMap operator and how to cancel requests with it stackoverflow.com/questions/56958362/ – Bladito Dec 13 '19 at 11:51
  • Instead of 'event instanceof ActivationEnd' would it make sense to use 'event instanceof NavigationStart' instead so as to abort the http calls a bit sooner? – XDS Mar 16 '20 at 22:50
  • What a neat way! Thanks mate – Victor Apr 30 '20 at 14:41
  • 1
    BEST COPY/PASTE CODE EVER :) n1 – Deunz Jul 17 '20 at 15:07
  • I like this but how do you avoid cancelling requests that are being made on the main app.component? – M27 Jan 07 '21 at 18:22
13

If you don't want to manually unsubscribe all subscriptions, then you can do this:

export function AutoUnsubscribe(constructor) {

  const original = constructor.prototype.ngOnDestroy;

  constructor.prototype.ngOnDestroy = function() {
    for (const prop in this) {
      if (prop) {
        const property = this[prop];
        if (property && (typeof property.unsubscribe === 'function')) {
          property.unsubscribe();
        }
      }
    }

    if (original && typeof original === 'function') {
      original.apply(this, arguments)
    };
  };

}

Then you can use it as decorator in your component

@AutoUnsubscribe
export class YourComponent  {
}

but you still need to store subscriptions as component properties. And when you navigating out of component, AutoUnsubscribe function will occurs.

Anton Lee
  • 684
  • 4
  • 10
  • 1
    I like this idea. Might I suggest you make it more robust by handling components which have arrays of subscriptions (not uncommon)?. E.g. `(Array.isArray(property) ? property : [property]).filter(property => isFunction(property.unsubscribe)).forEach(property => property.unsubscribe()));`. – Aluan Haddad Sep 18 '17 at 18:10
  • This is good idea, but need to optimize it, because if you have arrays of big data, then filter will search each element, and it may be a little bit slow. May be we can check only first element of array and if its subsctiption then we can assume that all array is subscriptions elements. – Anton Lee Sep 19 '17 at 02:43
  • you could do that but I doubt it's going to be a significant performance impact. – Aluan Haddad Sep 19 '17 at 02:56
7

I'm not convinced of the need for the functionality requested, but you can accomplish this, cancelling all outstanding requests whenever and wherever you wish by wrapping the framework's http service and delegating to it.

However, when we go about implementing this service, a problem quickly becomes apparent. On the one hand, we would like to avoid changing existing code, including third party code, which leverages the stock Angular http client. On the other hand, we would like to avoid implementation inheritance.

To get the best of both worlds we can implement the Angular Http service with our wrapper. Existing code will continue to work without changes (provided said code does not do anything stupid like use http instanceof Http).

import {Http, Request, RequestOptions, RequestOptionsArgs, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';



export default interface CancellationAwareHttpClient extends Http { }

export default class CancellationAwareHttpClient {
  constructor(private wrapped: Http) {
    const delegatedMethods: Array<keyof Http> = [
      'get', 'post', 'put', 'delete',
      'patch', 'head', 'options'
    ];
    for (const key of delegatedMethods) {
      this[key] = wrapped[key].bind(wrapped);
    }
  }

  cancelOutstandingRequests() {
    this.subscriptions.forEach(subscription => {
      subscription.unsubscribe();
    });
    this.subscriptions = [];
  }

  request(url: string | Request, options?: RequestOptionsArgs) {
    const subscription = this.wrapped.request(url, options);
    this.subscriptions.push(subscription);
    return subscription;
  }

  subscriptions: Subscription[] = [];
}

Note that the interface and class declarations for CancellationAwareHttpClient are merged. In this way, our class implements Http by virtue of the interface declaration's extends clause.

Now we will provide our service

import {NgModule} from '@angular/core';
import {ConnectionBackend, RequestOptions} from '@angular/http';

import CancellationAwareHttpClient from 'app/services/cancellation-aware-http-client';

let cancellationAwareClient: CancellationAwareHttpClient;

const httpProvider = {
  provide: Http,
  deps: [ConnectionBackend, RequestOptions],
  useFactory: function (backend: ConnectionBackend, defaultOptions: RequestOptions) {
    if (!cancellationAwareClient) {
      const wrapped = new Http(backend, defaultOptions);
      cancellationAwareClient = new CancellationAwareHttpClient(wrappedHttp);
    }
    return cancellationAwareClient;
  }
};

@NgModule({
  providers: [
    // provide our service as `Http`, replacing the stock provider
    httpProvider,
    // provide the same instance of our service as `CancellationAwareHttpClient`
    // for those wanting access to `cancelOutstandingRequests`
    {...httpProvider, provide: CancellationAwareHttpClient}
  ]
}) export class SomeModule {}

Note how we override the existing framework provided service. We use a factory to create our instance and do not add any decorators for DI to the wrapper itself in order to avoid a cycle in the injector.

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • I wanted to achieve that because even when I navigate through pages. the HTTP requests that are pending does not get canceled. I have to use destroy subscription on every page. I thought why not do it globally i.e., during a route change cancel all pending HTTP requests. – Sibiraj Sep 15 '17 at 08:14
  • correct me If what I was thinking to achieve is wrong :) – Sibiraj Sep 15 '17 at 08:18
  • @SibiRaj I don't think it's wrong :) just be careful when you introduce global state. The nice thing about this approach is that you can experiment with it without changing any of the services or components that use `Http`. – Aluan Haddad Sep 15 '17 at 08:25
  • Hi @AluanHaddad, where did you get `wrappedHttp` from? Your comment doesn't clarify this. Can you please share with us more details about? Thx – Dom Nov 25 '18 at 12:00
  • @Sibiraj I instantiated it directly. It is in the example but the code may need minor adjustments to work in angular 6/7 – Aluan Haddad Nov 25 '18 at 12:39
4

ngOnDestroy callback is typically used for any custom cleanup that needs to occur when the instance is destroyed.

where do you want to cancel your request?

maybe if you want cancel your requests on browser close there is creative idea here

Vala Khosravi
  • 2,352
  • 3
  • 22
  • 49
2

Try This :

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Rx';

export class Component implements OnInit, OnDestroy {
    private subscription: Subscription;
    ngOnInit() {
        this.subscription = this.route.params.subscribe();
    }
    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}
Sibiraj
  • 4,486
  • 7
  • 33
  • 57
Chandru
  • 10,864
  • 6
  • 38
  • 53
1
    //This is the example of cancelling the get request once you leave the TestComponent.

    import { Component, OnInit} from '@angular/core';

    @Component({
      selector: 'app-test',
      templateUrl: './test.component.html'
    })
    export class TestComponent implements OnInit {

      request: any;
someList: any;

      constructor( private _someService: SomeService) {

      }

    ngOnInit() {
        this.getList();
      }

      ngOnDestroy(){
        this.request.unsubscribe(); // To cancel the get request.
      }

      getList() {
        this.request= this._someService.getAll()
          .subscribe((response: any) => {
            this.someList= response;
          }, (error) => {
            console.log("Error fetching List", error);
          })
      }

    }
1

Adding something to @Bladito answer which is almost perfect.

Actually, the HttpCancelService stack is perfect, but the probleme is where it's called. Calling this on navigation end may cause problems if you have child routes.

So I made an abstract container component which call the HttpCancelService when it's destroyed. That way I can manage when I want to cut any Http Cancelling request with more fine grain.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { HttpCancelService } from '../../services/http-cancel-service.service';

@Component({
  selector: 'some-abstract-container',
  template: `
    ABSTRACT COMPONENT
  `,
  styleUrls: ['./abstract-container.component.scss']
})
export class AbstractContainerComponent implements OnInit, OnDestroy {
  constructor(protected readonly httpCancelService: HttpCancelService) {}

  ngOnInit() {}

  ngOnDestroy(): void {
    this.httpCancelService.cancelPendingRequests();
  }
}


And there it's a concrete component extending the abstract component:

import { Component, OnInit } from '@angular/core';
import { AbstractContainerComponent } from '../../../shared/components/abstract-container/abstract-container.component';
import { HttpCancelService } from '../../../shared/services/http-cancel-service.service';

@Component({
  selector: 'some-concrete-container',
  templateUrl: '.some-concrete-container.component.html',
  styleUrls: ['./some-concrete-container.component.scss']
})
export class SomeConcreteContainerComponent extends AbstractContainerComponent implements OnInit {
  constructor(protected readonly httpCancelService: HttpCancelService) {
    super(httpCancelService);
  }

  ngOnInit() {}
}

Deunz
  • 1,776
  • 19
  • 32
1

I don't think it's a good idea to cancel requests on route change level because one will lose granularity.

For instance, maybe you want to cancel a request on one component and not on another because it's not going to destory. Most importantly, what about background requests? It will be very trickey to debug why some requests have randomly been cancelled.

But its generally a good idea to cancel get requests whose component is going to destroy, regardless of route change.


Unsubscribing from observables on destroy

If you want to make your life easy then use until-destroy. It will automatically unsubscribe all observables when your component is going going to be destroyed (ngOnDestroy). It's granule enough and more general (not just HttpRequests but all observables will been unsubscribed from)

import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
     
@UntilDestroy()
@Component({})
export class InboxComponent {
  ngOnInit() {
    interval(1000)
      .pipe(untilDestroyed(this))
      .subscribe();
  }
}
Johan Aspeling
  • 765
  • 1
  • 13
  • 38
hanan
  • 1,768
  • 1
  • 14
  • 19
0

You can make a custom Http Service (using HttpClient) which maintains a list of pending requests. Whenever you fire a http us this custom service instead of Http/HttpClient,now push the subscriptions to a list and on return of the response pop that subscription out. Using this you will have all the incomplete subscriptions in a list.

Now in the same custom service Inject router in the constructor and subscribe on it to get the route change events. Now whenever this observable emits, all you need to do is to unsubscribe all the subscriptions present in the list and pop all the elements from it.

If you need code snippet, do mention in comment.

Sumit Agarwal
  • 4,091
  • 8
  • 33
  • 49
0

Here is a very simple, tested, and working example

import { Component, ViewChild, ElementRef } from "@angular/core";
import { DataService } from "../services/data.service";

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"],
})
export class HomePage {
  @ViewChild("image") image: ElementRef;
  loading = false;
  subscriptions = [];
  constructor(private dataService: DataService) {}

  start() {
    this.loading = true;
    this.image.nativeElement.classList.add("animate");
    const subscription = this.dataService
      .paymentRequest()

      .subscribe(
        (data) => {
          this.image.nativeElement.classList.remove("animate");
          this.loading = false;
          console.log(data);
        },
        (error) => this.start()
      );
    this.subscriptions.push(subscription);
  }

  stop() {
    this.loading = false;
    this.image.nativeElement.classList.remove("animate");
    this.subscriptions.forEach((subscription) => {
      subscription.unsubscribe();
    });
    this.subscriptions = [];
    // This completes the subject properlly.
  }
}
Krishna Karki
  • 749
  • 12
  • 31