spotipy-dev / spotipy

A light weight Python library for the Spotify Web API
http://spotipy.readthedocs.org
MIT License
5.01k stars 953 forks source link

POST playlist_add_items not retrying (HTTPError: 429 API rate limit exceeded) #577

Open stephanebruckert opened 4 years ago

stephanebruckert commented 4 years ago

All methods should be retrying but playlist_add_items doesn't seem to:

import spotipy

sp = spotipy.Spotify(auth_manager=spotipy.SpotifyOAuth(scope='playlist-modify-public playlist-modify-private',
                                                       show_dialog=True))

playlist = sp.user_playlist_create(user_id, "test", False)

i = 0
while i < 1000:
    some_track_id = sp.search('test')['tracks']['items'][0]['uri']
    sp.playlist_add_items(playlist['id'], [some_track_id], 0)
    i += 1
    print(i)
493
494
495
496
497
498
499
500
501
HTTP Error for POST to https://api.spotify.com/v1/playlists/11k8jzmrwudvDnjnhfgP76/tracks returned 429 due to API rate limit exceeded
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/spotipy/client.py", line 244, in _internal_call
    response.raise_for_status()
  File "/Library/Python/3.8/lib/python/site-packages/requests/models.py", line 941, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 429 Client Error: unknown for url: https://api.spotify.com/v1/playlists/11k8jzmrwudvDnjnhfgP76/tracks?position=0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test_spotipy.py", line 21, in <module>
    sp.playlist_add_items('11k8jzmrwudvDnjnhfgP76', [some_track_id], 0)
  File "/usr/local/lib/python3.8/site-packages/spotipy/client.py", line 1015, in playlist_add_items
    return self._post(
  File "/usr/local/lib/python3.8/site-packages/spotipy/client.py", line 289, in _post
    return self._internal_call("POST", url, payload, kwargs)
  File "/usr/local/lib/python3.8/site-packages/spotipy/client.py", line 259, in _internal_call
    raise SpotifyException(
spotipy.exceptions.SpotifyException: http status: 429, code:-1 - https://api.spotify.com/v1/playlists/11k8jzmrwudvDnjnhfgP76/tracks?position=0:
 API rate limit exceeded, reason: None

Tried with 2.15.0 and 2.10.0

Quizz1Cal commented 4 years ago

Hey @stephanebruckert I wasn't able to replicate this issue in my environment (spotipy==2.15.0, ubuntu 20.04.1 LTS, xterm) - though that might be because our networks are different. I think the root issue is that spotipy can't currently handle API rate limit exceeding as we're raising exceptions on any HTTPError. A solution might exist in the parsing of response.json()['Retry-After'] (see this link) but this would be hard to test without the error actually being thrown. Any ideas on replicable code to (quickly) exceed the rate limit?

stephanebruckert commented 4 years ago

Try this:

(by the way, to test it against the main branch on your local clone, the script just needs to be placed at the root of the project)

from concurrent.futures import ThreadPoolExecutor
import spotipy

TOTAL_ITEMS_TO_ADD = 400
MAX_WORKERS = 50
PLAYLIST_OWNER_ID = "your_user_id"

# Spotify init
sp = spotipy.Spotify(auth_manager=spotipy.SpotifyOAuth(scope='playlist-modify-public playlist-modify-private',
                                                       show_dialog=True))
some_track_id = sp.search('test')['tracks']['items'][0]['uri']
playlist = sp.user_playlist_create(PLAYLIST_OWNER_ID, "test", False)
ids_to_add = [some_track_id] * TOTAL_ITEMS_TO_ADD

def add_item(track_id):
    # It does not work with POST:
    sp.playlist_add_items(playlist['id'], [track_id], 0)

    # It works with GET
    # sp.search('test')

    print("Success")

with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
    pool.map(add_item, ids_to_add)
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
Success
Success
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
Success
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded
Success
HTTP Error for POST to https://api.spotify.com/v1/playlists/0njyczNVNKFynnlzM6Tm9y/tracks returned 429 due to API rate limit exceeded

I think here Requests is handling retry-after for us, so we shouldn't need to do anything around that:

https://github.com/plamere/spotipy/blob/4bb42598e1e8e5411e36108af32bcc68acfa03aa/spotipy/client.py#L201

Also it works well for GET as it correctly retries:

Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Max Retries reached
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success
Success

I wonder, why does it work for GET and not for POST, knowing that both our GET and POST methods call the same _internal_call:

https://github.com/plamere/spotipy/blob/4bb42598e1e8e5411e36108af32bcc68acfa03aa/spotipy/client.py#L290

https://github.com/plamere/spotipy/blob/4bb42598e1e8e5411e36108af32bcc68acfa03aa/spotipy/client.py#L295

PS: I just found this https://stackoverflow.com/a/35707701/1515819, that might be our solution!

stephanebruckert commented 3 years ago

@Quizz1Cal @ritiek please review this fix if you get a chance https://github.com/plamere/spotipy/pull/596

Beahmer89 commented 3 years ago

@stephanebruckert I think its worth noting that there is a reason that urllib does not do retries for POSTS and that is because they are not considered safe or idempotent. GET, PUT, and DELETE fall into these categories but POST however does not. More detail can be found in RFC7231. Having retries on POSTs can have unintended consequences like having multiple records created in the database if there was an error upon creation.

It is my personal opinion that this should not have been created as it is not an issue for the following reasons:

Please let me know if I am missing something and I am open to hearing out the reasons for the change, but I figured I would add my 2 cents as I helped contribute the change to the retry logic.

stephanebruckert commented 3 years ago

Sorry for the late response I was bit busy recently. Thanks also for the inputs that's all fair points. It's my bad, I should have waited for feedback on the PR, but I'm happy to discuss it here now and am also happy to revert things if needed

Answering your points in order:

Great to discuss this!!

stephanebruckert commented 3 years ago

Related to the first point about always retrying on 429, I just found we could use respect_retry_after_header

respect_retry_after_header (bool) – Whether to respect Retry-After header on status codes defined as Retry.RETRY_AFTER_STATUS_CODES or not. https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html

I'm going to open a PR to set respect_retry_after_header to true and revert the change on method_whitelist as you suggested

Edit: did a quick test and doesn't seem to work

Beahmer89 commented 3 years ago

@stephanebruckert no worries! Everyone gets busy and I am late to responding as well. I am glad we are talking about this as I always find this topic interesting! It seems like your main reason was to have to respect the Retry-After header if present and I think that is fair and we should do the work to ensure this is enabled for devs by default.

After looking into it:

Also just to clear some things up in the comment with bullet points:

So where does this leave us?

Again really enjoying talking about this :) good stuff to think about and a fun topic! Let me know your thoughts and if anything I said doesnt make sense or is off base!

Mokson commented 3 years ago

I'm having the same issue and decided to downgrade to spotipy==2.4.4 where I know it works for sure.

retrying ...1secs

shillshocked commented 2 years ago

I'm getting the same issue occasionally in my scripts. I'm wondering if it's an issue at Spotify's end?