New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handling Rate Limiting With v2 Commands #436
Comments
Hi @CyTheGuy, thanks for opening this Github issue! Providing support for better handling of rate limits is something that we're definitely interested in exploring. This is a feature we want to add, but it's important that we do so in a way that doesn't make it easy to abuse the API. I'm planning on spending more time on this over the next couple of weeks. If you'd like to fix it locally in the time being here's some pseudo code that should get you close: max_attempts = 3
for i in range(max_attempts):
while True:
try:
client.chat_postMessage() # Whatever API call you're making.
except client_err.SlackApiError as exception
wait_time = float(exception.response["headers"]["Retry-After"])
time.sleep(wait_time) # or await asyncio.sleep(wait_time) for async methods.
continue
break This has also helped me identify an existing bug where the amount of time you should wait is not provided in the exception. I'll likely work on this very soon. |
Woohoo! Thank you for getting back so quickly. I will give this a try thank you so much. Didn't realize I could handle the error myself like that. Previously I would just see if the error returned was "ratelimited" and sleep if it wasn't "ok" but this looks like a solid path forward for now. Thanks again |
@RodneyU215 Howdy, just wanted to point out a small wrinkle in this strategy of throwing Exceptions for rate limiting. The new Response object is a generator / iterable, which is super useful for paging through for response in sc.api_call("conversations.list", params=params):
... I can't catch the UPDATE I can't even do that successfully because in def gen_wrapper(response):
while True:
try:
# Returns Response object
yield next(response)
except StopIteration:
raise
except SlackApiError as exn:
# rate limiting code here Will error |
I am also unable to handle any rate limit issues with the newest slackclient for python.
However, the SlackApiError response dict has no headers key.
I've tried getting the headers from the It seems like there is no way to gracefully handle rate limit errors using the python client? How do I get the headers from the SlackApiError object? |
So I've just encountered that thing and now wondering how to get around. |
I really don't see any way to handle rate limiting in this new version. Can someone show a working example of a loop where you hit the rate limit and then you handle it? My goal is to do the following:
I always hit the rate limit half way through the user list but I haven't figured out how to handle it proper and proceed. How do you retrieve the response headers after you've received the exception? |
@lukwam It is broken (no offence, just stating the fact). I think for any fairly long-term project it is worth to use Web API directly ( I had to |
Yeah I honestly just decided to write everything in Go now and use https://github.com/nlopes/slack instead |
https://github.com/os/slacker might be worth a try as well, though not sure how full featured it is. |
Thanks for all of the feedback! I appreciate it. I'm going to be working on an update to address the challenges mentioned here. |
It was working well for me, until I found that the |
This used to work in v1. Here's how I updated my prior loop to work with the broken v2. I add a second delay on every error and subtract one thousandth on every success. (In v1 I delayed the result of "Retry-After").
and the loop that calls the method above
|
So, actually we need raw response in exception I wrote workaround for that: from slack.errors import SlackApiError
class SlackApiError2(SlackApiError):
def __init__(self, message, response, raw_response):
super().__init__(message, response)
self.raw_response = raw_response
@classmethod
def from_original(cls, exc: SlackApiError):
response_validate_frame = next(
frame for frame, _ in traceback.walk_tb(exc.__traceback__)
if (
frame.f_code.co_name == 'validate' and
frame.f_code.co_filename.endswith('slack_response.py')
)
)
message, _ = exc.args[0].split('\n', maxsplit=1)
response = exc.response
raw_response = response_validate_frame.f_locals['self']
return cls(message, response, raw_response) Then in application code: try:
response = self.client.api_call(method, json=json, data=data, params=params, files=files)
except SlackApiError as e:
# here you can reraise exception, but i need it below
response = SlackApiError2.from_original(e).raw_response
# handle response |
Can you at least return the headers and status code when raising the Exception @RodneyU215 ? 😭 def validate(self):
"""Check if the response from Slack was successful.
Returns:
(SlackResponse)
This method returns it's own object. e.g. 'self'
Raises:
SlackApiError: The request to the Slack API failed.
"""
if self.status_code == 200 and self.data.get("ok", False):
self._logger.debug("Received the following response: %s", self.data)
return self
msg = "The request to the Slack API failed."
raise e.SlackApiError(message=msg, response=self.data) |
If raw response will be present in SlackApiError, then we can get any data: status_code, headers, data, anything |
Thanks for everyone's help and patience on this issue! 🙇 I've just released 2.2.1 and which includes a fix for this. As shown in the test below your exception will have a response variable. This is the def test_slack_api_rate_limiting_exception_returns_retry_after(self, mock_request):
mock_request.response.side_effect = [
{"data": {"ok": False}, "status_code": 429, "headers": {"Retry-After": 30}}
]
with self.assertRaises(err.SlackApiError) as context:
self.client.api_test()
slack_api_error = context.exception
self.assertFalse(slack_api_error.response["ok"])
self.assertEqual(429, slack_api_error.response.status_code)
self.assertEqual(30, slack_api_error.response.headers["Retry-After"]) |
Thanks a lot man @RodneyU215 ! This saved me from my horrific workaround code! |
@zxul767 Hello, just in case you're interested - I have forked slacker and merged the abandon PR there. New package is available in Pypi by name "Slacker2" |
@RodneyU215 thank you for the example! Perhaps it would be nice to update the example in the web api rate limits section of the documentation as it is somewhat misleading right now. |
Hi, I tried with the example code but I keep getting a response from the API without ["headers"]["Retry-After"].
I have slackclient==2.5.0 Am I missing something? |
@AlexisMundu As discussed above, the document should be updated (I will work on it soon #640). You can access the header value the following way. from slack import WebClient
from slack.errors import SlackApiError
token = os.environ['SLACK_BOT_TOKEN']
webclient = WebClient(token=token)
try:
result = webclient.conversations_list()
except SlackApiError as e:
print(e.response.headers['Retry-After']) |
Oh I see, thanks a lot @seratch 👍 |
@seratch I've found that whenever I hit the rate limit, the response headers always have Retry-After = 1, which is clearly not enough time. Further, even if I wait for 30 seconds instead, I've found that it can sometimes take 6+ attempts (so 3 minutes of waiting) before returning successful responses again. The methods I'm using are all in Tiers 3 and 4, so I'm a bit confused how I'm even hitting the limit in the first place and how it takes so long to cool down. Any thoughts? Thanks! |
@neelraman Can you share the list of methods (no need to be a complete list)? I can verify with those, just in case. |
@seratch thanks for the quick response! I did some further research, and it's actually only conversations.replies that is consistently giving me Retry-After=1 when I hit the rate limit. |
@neelraman I've confirmed this with the server-side teams. The If retries after 1 second in the case don't work for you, could you submit an inquiry to our support team? They can investigate more deeply by accessing the data of your apps and workspaces. |
@neelraman @seratch has there been any conclusive investigation regarding the issue with I recently updated a python script I have used since 2017 so that I could replace the deprecated 'im.history' and 'groups.history' methods, and I am now running into this same problem that @neelraman reported. |
Actually, in my case the retry-after 1 second is being honored. As a quick test, I edited my script to sleep for 2 seconds whenever this happens, and now I see a pattern of four requests succeeding, then the fifth being rate limited, then 2 seconds sleep. Then repeat ad infinitum: 4 succeed, 5th one ratelimited, sleep for 2 seconds. This is holding for approximately 40 minutes straight just now. So perhaps the |
To anyone who finds this issue, check out https://slack.dev/python-slack-sdk/web/index.html#retryhandler |
Description
What is the best way to handle web API rate limiting with the new v2 client? The docs https://slack.dev/python-slackclient/basic_usage.html [slack.dev] don't seem to work anymore and the release notes and migration notes didn't suggest anything. I now get this error when hitting rate limiting but there appears no way to handle it like before since the response is never returned to me it is just output as an error.
slack.errors.SlackApiError: The request to the Slack API failed.
The server responded with: {'ok': False, 'error': 'ratelimited'}
What type of issue is this? (place an
x
in one of the[ ]
)Requirements (place an
x
in each of the[ ]
)Reproducible in:
slackclient version: v2
python version: Python 3.7.3
OS version(s): Mac
Logs, screenshots, screencast, sample project, funny gif, etc.
The text was updated successfully, but these errors were encountered: