10

I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.

There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.

Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.

Basically this is the behaviour I am trying to do with an interceptor:

factory('httpService', ['$http', function($http) {

    var pendingCalls = {};

    var createKey = function(url, data, method) {
        return method + url + JSON.stringify(data);
    };

    var send = function(url, data, method) {
        var key = createKey(url, data, method);
        if (pendingCalls[key]) {
            return pendingCalls[key];
        }
        var promise = $http({
            method: method,
            url: url,
            data: data
        });
        pendingCalls[key] = promise;
        promise.finally(function() {
            delete pendingCalls[key];
        });
        return promise;
    };

    return {
        post: function(url, data) {
            return send(url, data, 'POST');
        }
    }

}])

When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.

Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?

ivarni
  • 17,658
  • 17
  • 76
  • 92
  • I think that was answered here: http://stackoverflow.com/a/17328336/2938008 – maurycy Feb 28 '14 at 11:03
  • Nope. That approach would require me to go around and add a timeout-promise in every single class that uses $http which is exactly what I am trying to avoid having to do. It would also cancel the request instead of preventing it from happening. – ivarni Feb 28 '14 at 11:48
  • Are you looking for a viewmodel way ($http.interceptor) of doing this as opposed to using the view? Like disabling the button when it is clicked, and re-enable when you receive your success callback? – jcc Aug 05 '14 at 19:54
  • 2
    @user1518802 Not really, I was hoping to find a way to do this with interceptors to stop having to make everyone in the team remember to inject a custom service instead of using $http directly. I feel that disabling buttons is more of a UX concern while this is more of a way to mitigate problems with duplicate requests to the backend. We do replace buttons with spinners when they're clicked but that is done with a directive. I actually don't think this question as it stands has a solution. – ivarni Aug 06 '14 at 04:27
  • https://github.com/witoldsz/angular-http-auth This is something we use to intercept all http calls and validate them with auth. Maybe you can find some way of suiting it to your needs. – jcc Aug 06 '14 at 16:13

2 Answers2

1

according to $http documentation, you can return your own config from request interceptor.

try something like this:

config(function($httpProvider) {
    var cache = {};

    $httpProvider.interceptors.push(function() {
        return {
            response : function(config) {
                var key = createKey(config);
                var cached = cache[key];
                return cached ? cached : cached[key];    
            }
        }
    });
}
ilj
  • 859
  • 8
  • 18
  • I'm not really looking for a cache, two GET requests at two different points in time might return different results. What I was hoping to achieve was to cancel a request to any given resource if there was already one in place. I played around with your approach in plnkr but all I really achieved was to cache a response while still triggering duplicate requests. If you look carefully at the service I posted it actually stops a second request from happening but only while a duplicate request has not returned. It also feeds both callers the same promise. It's not a caching mechanism. – ivarni Aug 07 '14 at 06:38
  • 1
    @ivarni, yes, you can't handle promises with interceptors. for this case your Model pattern approach seems valid. i'd change 'promise.then' to 'promise.finally' though. – ilj Aug 07 '14 at 06:49
  • That's a VERY good point :) Luckily we eject users after any failed HTTP call but if we didn't that could have caused some nasty issues. – ivarni Aug 07 '14 at 06:57
  • @ilj can you elaborate why this is the case? – hugo der hungrige Sep 25 '14 at 19:11
  • 1
    @hugoderhungrige `promise.then` (with no error-callback) only gets executed on a succesful request, while `promise.finally`gets executed on all requests. In this case the pending request should be purged also if it fails. Be a bit careful though, older versions of IE does not approve of the use of `finally` in that context as it's a reserved word. I can't recall if it's a problem with IE8 or IE9 though. – ivarni Sep 29 '14 at 11:34
  • 1
    @ivarni, can't you use promise['finally'] syntax for it? – ilj Oct 01 '14 at 00:45
  • @hugoderhungrige, not sure what exactly you were asking for. did ivarni's answer help? – ilj Oct 01 '14 at 00:47
1

Very old question, but I'll give a shot to handle this situation.

If I understood correctly, you are trying to:

1 - Start a request and register something to refer back to it;

2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.

This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:

function DropoutInterceptor($injector) {
    var $q = $q || $injector.get('$q');
    var dropouts = {};

    return {
        'request': function(config) {
            // I'm using the request's URL here to make
            // this reference, but this can be bad for
            // some situations.
            if (dropouts.hasOwnProperty(config.url)) {
                // Drop the request
                dropouts[config.url].resolve();
            }

            dropouts[config.url] = $q.defer();

            // If the request already have one timeout
            // defined, keep it, othwerwise, set up ours.
            config.timeout = config.timeout || dropouts[config.url];

            return config;
        },
        'requestError': function(reason) {
            delete dropouts[reason.config.url];

            return $q.reject(reason);
        },
        'response': function(response) {
            delete dropouts[response.config.url];

            return response;
        },
        'responseError': function(reason) {
            delete dropouts[reason.config.url];

            return $q.reject(reason);
        }
    };
}
Mateus Leon
  • 1,381
  • 1
  • 14
  • 21