Skip to content
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

Feature Request: Retry-After Header support for rate limiting #10

Open
rob-buskens-sp opened this issue May 7, 2024 · 3 comments
Open

Comments

@rob-buskens-sp
Copy link

Feature request

Incorporate support for Identity Security Cloud (ISC) API rate limits.

If ISC APIs return a 429 then try again after the number of seconds in the Retry-After header.

Rate Limits

There is a rate limit of 100 requests per access_token per 10 seconds for V3 API calls through the API gateway. If you exceed the rate limit, expect the following response from the API:

HTTP Status Code: 429 Too Many Requests

Headers:

Retry-After: {seconds to wait before rate limit resets}

Sample code coming?

I believe I've implemented this previously and will look for the code as an example. But don't wait for me! lol.

@rob-buskens-sp
Copy link
Author

I found my code and it used https://urllib3.readthedocs.io/en/2.2.1/reference/urllib3.util.html which is what the python sdk appears to use for retries.

I had configured with a back off factor to wait longer on retries.

    retries = 3
    backoff_factor = 0.3
    retry_http_codes = (500, 502, 504, 429)
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist = retry_http_codes
    )

I didn't do any testing regarding it handling 429 responses and the retry-after header.

@rob-buskens-sp
Copy link
Author

I did some testing and it appears that urllib3.Retry handles 429 and the Retry-After header with seconds.
I didn't try dates.

@rob-buskens-sp
Copy link
Author

express mock server

GET /api/users returns a 429 every 2nd invocation with varying waits

app.js

const app = express();
const port = 3000;

const routes = require('./routes');

app.use('/api', routes);

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

routes.js

const express = require('express');
const router = express.Router();

// Define routes
retry = false

// min and max included 
function randomIntFromInterval(min, max) { 
  return Math.floor(Math.random() * (max - min + 1) + min);
}

router.get('/users', (req, res) => {
  if (retry) {
    retryAfter = randomIntFromInterval(10, 60)
    console.log(`retry-after ${retryAfter}`)
    res.appendHeader('Retry-After', retryAfter).sendStatus(429);
  } else {
    console.log('200 all good')
    res.status(200).send({ status: 200, message: "all good"})
  }

  retry = !retry
});

router.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Details of user ${userId}`);
});

router.post('/users', (req, res) => {
  res.send('Create a new user');
});

router.put('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Update user ${userId}`);
});

router.delete('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Delete user ${userId}`);
});

module.exports = router;

python test client with Retry

import urllib3
import logging
import time

from urllib3 import Retry

if __name__ == '__main__':
  logging.basicConfig(filename='retry.log',
                      filemode='a',
                      format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
                      datefmt='%H:%M:%S',
                      level=getattr(logging, "DEBUG"))
  
  requests_log = logging.getLogger("requests.packages.urllib3")
  requests_log.setLevel('DEBUG')
  requests_log.propagate = True

  timeout = urllib3.util.Timeout(connect=2.0, read=7.0)

  retries = Retry(
    total=5, status_forcelist = [ 502, 503, 504 ],
    respect_retry_after_header=True
  )

  http = urllib3.PoolManager(timeout=timeout, retries=retries)

  for x in range(100):
    
    resp = http.request("GET", "http://localhost:3000/api/users")
    
    print(x, resp.status)

Test

python client makes 100 calls, urllib3 logging shows every second call is a 429 and the retry occurring while the program output shows only the successful processing.

program output

0 200
1 200
2 200
3 200
4 200
5 200
6 200
7 200

log output

18:18:36,124 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:19:30,134 urllib3.connectionpool DEBUG Retry: /api/users
18:19:30,136 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:19:30,142 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:19:30,145 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:19:30,145 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:19:40,155 urllib3.connectionpool DEBUG Retry: /api/users
18:19:40,156 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:19:40,162 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:19:40,164 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:19:40,165 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:20:38,175 urllib3.connectionpool DEBUG Retry: /api/users
18:20:38,177 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:20:38,189 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:20:38,191 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:20:38,192 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:21:15,202 urllib3.connectionpool DEBUG Retry: /api/users
18:21:15,204 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:21:15,213 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0

I suggest you'll want to do your own testing. On confirmation the feature request is to update the python-sdk doc: https://developer.sailpoint.com/docs/tools/sdk/python

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant