5

I have seen lots and lots of posts on this topic, and lots of web articles, but I remain stumped with my problem. I found this post very useful but my timeoutRequest() function is never being called.

I am using a promise with the timeout property of $http but the underlying HTTP request is not being cancelled. I think the promise itself is being resolved but the request is not cancelled.

My controller behaviours looks like this:

$scope.enquiriesSelected = function() {
    $scope.cancelHttpRequests();
    $location.path("/enquiries");
};

$scope.cancelHttpRequests = function () {
    console.log(canceller.promise.$$state);
    canceller.resolve("User cancelled");
    console.log(canceller.promise.$$state);
};

My HTTP request promise looks like this:

var canceller = $q.defer();

$scope.searchResultsPromise = $http({
        url: "/api/customers/customersearch",
        method: "POST",
        data: criteria,
        timeout: canceller.promise 
    })
    .success(function(data) {
        $scope.customerSearchResults = data;
    });

I have tried various methods to get this to work, including putting the canceller in the $scope.

I have been looking through the AngularJS source code and I find these lines:

if (timeout > 0) {
    var timeoutId = $browserDefer(timeoutRequest, timeout);
} else if (isPromiseLike(timeout)) {
    timeout.then(timeoutRequest);
}

function timeoutRequest() {
    jsonpDone && jsonpDone();
    xhr && xhr.abort();
}

However the execution path does not reach these lines when my promise is resolved. The xhr.abort() is never called and this is the only place in the AngularJS source code that aborts an HTTP request.

Inspecting with the console in F12 (Chrome) when trying to cancel the request reveals that the $$state of the promise changes from 0 to 1 so I'm reasonably sure that the promise is resolving. However in network traffic the HTTP request is not being cancelled. I cannot navigate to another page until the HTTP request is completed.

Can anyone help?

M

Community
  • 1
  • 1
serlingpa
  • 12,024
  • 24
  • 80
  • 130
  • If your request taking long, I would probably make a boolean on server side, saying data is loaded/ready, and you can check every second if boolean is true, get data back. Two advantages here: user can cancel timer and avoid checking data ready, browser does not hang over. – Dmitri Algazin Jun 02 '15 at 09:09
  • I can't do that I'm afraid. I'm calling a service for which I don't have the source code. – serlingpa Jun 02 '15 at 09:11
  • Why do you save the promise to $scope.searchResultsPromise ? If you remove that, does that help? – Vlad274 Jun 04 '15 at 16:31
  • The problem here is fairly simple theoretically but very complicated to implement, Imagine a client server architecture. Once the client is done sending request It's up-to server to timeout that request. So you have to code it on server side to allow a optional parameter with timeout milliseconds as value, If set the server will send error with timeout if that request processing takes more time than timeout. but even in this scenario, the communication time lapse is not factored in. The timeout set on client side only returns error for the promise and not handle response – Gurbakhshish Singh Jun 04 '15 at 17:07
  • Do you know why you can't navigate away if the request is in progress? It doesn't sound typical to me... What browser are you using, and is it specific to that one? – Michal Charemza Jun 04 '15 at 18:43
  • 1
    To try to narrow this down, can you recreate this issue in a completely bare-bones Angular app, with one controller, only this http request being fired, and post the code + a link to it working (/not working) somewhere? – Michal Charemza Jun 05 '15 at 06:39
  • @MichalCharemza is completely right: please create the simplest way to recreate your problem. I am 99,9% sure the problem is in your code. Since it seems that your browser hangs up, one possible explanation may be that you are doing a **synchronous** xhr call. In such a case no other code has a chance to run before the call has completed. – artur grzesiak Jun 05 '15 at 07:34
  • @Vlad274, the reason I save the promise to the $scope is that the resolution of this promise is used in the HTML to hide a "please wait..." message while the content is loading. – serlingpa Jun 05 '15 at 09:34
  • I will create this in Plunker and let you know. – serlingpa Jun 05 '15 at 09:35

1 Answers1

8

What you are describing is actually the correct way of cancelling an $http request using the timeout property. I have created this Plunkr which showcases this behaviour being as close as possible to the code you have provided. You can see that when the "Submit request with timeout" button is clicked a request is submitted and the timeout promise is resolved after 100 msec (since the request returns so fast that you don't really have time to click something to cancel it):

$scope.requestWithTimeout = function() {
  $timeout(function(){canceller.resolve("User cancelled");}, 100);
  $scope.submitRequest();
}

The $http request error callback checks the status of the response and displays an appropriate error message, in which you can see that the request was actually cancelled after 100msec. You can also verify this by observing the request in the network tab of your browser's developer tools.

$scope.submitRequest = function() {
    // Reset the canceler promise
    canceller = $q.defer();
    $scope.status = 'Request submitted';
    var startTime = (new Date()).getTime();
    $http({ 
      url: 'style.css',
      method: 'GET',
      timeout: canceller.promise 
    })
    .success(function(data) {
      var endTime = (new Date()).getTime();
      $scope.status = 'Request response returned after ' + (endTime - startTime) + ' msec.';
    })
    .error(function(data, status) {
      var endTime = (new Date()).getTime();
      if(status === 0) {
        $scope.status = 'Request timed out after ' + (endTime - startTime) + ' msec.';
      } else {
        $scope.status = 'Request returned with error after ' + (endTime - startTime) + ' msec.';
      }
    });
  }

Hopefully you can use this as an example to find what is wrong with your code and correct it. Otherwise please create a Plunkr (or something similar) with the code that is not working so that I can help you further.

Christina
  • 3,562
  • 3
  • 22
  • 32
  • Ok I will create a Plunkr and post the URL here. Thanks. – serlingpa Jun 05 '15 at 09:55
  • It's difficult because the HTTP request returns quite quickly; I'm on the hunt for a long-running service call. – serlingpa Jun 05 '15 at 10:07
  • Ok Christina, I have forked your Plunkr at http://plnkr.co/edit/zhhYjyIZdNdUjYxHFn5U. It's not the prettiest but it does demonstrate that it does in fact work, so I have to figure out what's different between my code and this sandbox code. – serlingpa Jun 05 '15 at 10:40
  • @serlingpa Are you sure it works? From what I can see the resource you are trying to access through the `$http` request is not accessible to the plunkr (due to CORS restrictions) so the request fails before the user has a chance to cancel it. That is not to say of course that it wouldn't work if the resource was accessible, it's just that in this particular case the request fails because of something else. – Christina Jun 05 '15 at 11:31
  • Hmm...you're right. BUT the network tab shows a status of cancelled rather than completed, so I'm hoping it works! I can't seem to find a large-ish PDF online avoiding CORS. – serlingpa Jun 05 '15 at 11:41
  • My current problem is that $scope.canceller is undefined when it hits the $scope.cancelHttpRequests behaviour which is a bit of a head-scratcher. – serlingpa Jun 05 '15 at 11:43
  • @serlingpa Where exactly is `$scope.canceller` defined? I assume that the first two code blocks you have provided (functions and `$http` request) are both part of the same controller. Is this not the case? Could you perhaps provide the whole controller code so that I can see what is declared where? – Christina Jun 05 '15 at 11:56
  • I have tried lots of places to but the canceller. It is in $scope, and it has also been a local "var canceller;" in the controller. I can't really post the whole controller without violating a NDA... – serlingpa Jun 05 '15 at 12:46
  • @serlingpa OK, in general do note that you don't need the canceller in the `$scope` unless you intend to use it in your HTML. All you need is the code declaring the canceler to have been executed (and therefore the canceler declared) at the time the function that calls it is executed. From what you are saying all I can assume is that the canceler declaration is not yet executed when the `$scope.cancelHttpRequests` function is called, but without a sample I'm afraid I can't help more... – Christina Jun 05 '15 at 13:06
  • I understand that Christina. I put the canceller in $scope just so I could have {{ canceller | json }} on my page. I will create a Plunkr with more flesh (similar to the controller that I'm using) that exhibits the same problem. – serlingpa Jun 05 '15 at 15:32