spotify-web-api-node icon indicating copy to clipboard operation
spotify-web-api-node copied to clipboard

Support for Retry After?

Open aymather opened this issue 3 years ago • 2 comments

There seems to be a common theme with the Spotify API which is that their rate limits are difficult to understand, which is why many people have investigated throttling techniques.

In my own implementation, I eventually came to a point with my own throttling where I would still randomly get 429 errors every once in a while. I wanted to avoid these as much as possible, so I eventually implemented my own try/catch on 429 errors for each request, which would extract the number of seconds my client would need to try the request again, wait that amount of time, and fire off the request after the delay.

I feel like this could be an extremely useful feature of this client. It could just be a parameter that you pass into the client instantiation (i.e. retryAfter=True) which would handle these for you.

Another solution would be to have it be an option that you pass per individual request (i.e. spotifyClient.getAlbum(id, { retryAfter: True }))

I feel like this makes sense to be handled in the client, but if you think it should be handled by the user, I'm curious why.

Thank you for this package! Cheers

aymather avatar Jan 08 '21 17:01 aymather

Yea I really want this. And @aymather can u please give the code you used for this Feature. I am little bit lost :/

Nishant1500 avatar Jan 11 '21 07:01 Nishant1500

@Nishant1500 I wrapped this package in a class, and added these 2 methods to my class.

    request = (client, type, param) => {
        switch(type) {
            case RequestTypes.Albums:
                return client.getAlbums(param);
            case RequestTypes.ManyAudioFeatures:
                return client.getAudioFeaturesForTracks(param);
            case RequestTypes.SearchTracks:
                return client.searchTracks(param);
            case RequestTypes.Artists:
                return client.getArtists(param);
            default:
                return new Promise(r => r({ body: undefined }));
        }
    }

    /**
     * This wrapper is to prevent against 429 errors.
     * @param {*} client 
     * @param {*} type 
     * @param {*} param 
     */
    retryWrapper = (client, type, param) => {

        return new Promise((resolve, reject) => {

            this.request(client, type, param)
                .then(data => resolve(data))
                .catch(err => {

                    // If we get a 'too many requests' error then wait and retry
                    if(err.statusCode === 429) {
        
                        setTimeout(() => {

                            this.request(client, type, param)
                                .then(data => resolve(data))
                                .catch(err => reject(err))
        
                        }, parseInt(err.headers['retry-after']) * 1000 + 1000);
        
                    }

                })

        })

    }

And then you're also going to need this for the request method.

const RequestTypes = {
    AudioFeatures: 'AudioFeatures',
    ManyAudioFeatures: 'ManyAudioFeatures',
    Album: 'Album',
    Albums: 'Albums',
    Artist: 'Artist',
    Artists: 'Artists',
    SearchTracks: 'SearchTracks'
}

The reason I'm passing the client into the retryWrapper is because I was also using multiple clients with different app_ids and stuff at the same time so I could make more requests.

The reason I have this request method, is so that you can pass any function to this retry wrapper to call on the client. You can't just pass the method itself into the retry wrapper because the client calls itself inside the methods, so you have to just tell the client what to execute inside the request method.

This was just my strategy, but I'm sure there's a much better way to do it. In hindsight, I'd probably try to subclass the package client, or just modify the package directly. But this still works.

aymather avatar Jan 14 '21 19:01 aymather