feat: add retry with backoff for rate limits and FetchAllOrgDevices helper#3
feat: add retry with backoff for rate limits and FetchAllOrgDevices helper#3robbiet480 wants to merge 5 commits intozchee:mainfrom
Conversation
…elper Apple's ABM API has unpublished rate limits and returns HTTP 429 when exceeded. Add exponential backoff retry (up to 5 retries, starting at 1s) to doJSONRequest so all API calls automatically handle rate limiting. Also add FetchAllOrgDevices which fetches all org devices following pagination, returning the full device list and total count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
not sure exponential backoff is necessary/good response has a def request(self, method, url, **kwargs):
while True:
response = super().request(method, url, **kwargs)
if response.status_code == requests.codes.too_many_requests:
time.sleep(int(response.headers.get('retry-after')))
elif response.status_code == requests.codes.unauthorized:
self.get_token()
else:
return response |
|
Yep, was unaware since it didn't appear mentioned in the docs so going to change this PR to support the header |
Apple's ABM API returns a Retry-After header (in seconds) with 429 responses. Parse and use it instead of always falling back to the exponential backoff, which was too aggressive for the device listing endpoint (Apple sends Retry-After: 60) and too slow for the AppleCare endpoint (Apple sends Retry-After: 1). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Added a commit to respect the Apple's ABM API returns
The previous pure exponential backoff (2s → 4s → 8s) was too aggressive for device listing (kept getting re-429'd) and unnecessarily slow for AppleCare. The exponential backoff is kept as a fallback for any 429 without the header. |
Apple's ABM API has an observed limit of ~20 requests/minute. Add a token bucket rate limiter (1 req/3s, burst 1) at the transport level so both doJSONRequest and PageIterator are covered. This avoids triggering 429s in the first place, which is better throughput than blasting requests and then waiting 60s on the penalty. The rate limiter sits below the oauth2 transport so auth token refreshes are not rate-limited. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Added proactive rate limiting via a Approach: Token bucket at 1 req/3s (burst 1) = ~20 req/min, matching Apple's observed rate limit. The limiter sits at the transport level (below Why proactive? Without it, the client blasts requests, hits a 429, and waits 60s — effectively ~20 requests per ~70s. With the limiter, requests are spaced at 3s intervals and 429s are mostly avoided, giving better sustained throughput. The |
The ABM API returns MAC address fields (wifiMacAddress, bluetoothMacAddress, ethernetMacAddress) as either a single string or an array of strings depending on the device. Using plain []string causes unmarshal failures when the API returns a string. FlexStringSlice handles both formats. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ingSlice ethernetMacAddress is always returned as an array from the ABM API. Only wifiMacAddress and bluetoothMacAddress are returned as bare strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
doJSONRequestnow automatically retries with exponential backoff (1s, 2s, 4s, 8s, 16s) up to 5 times on 429 responses, respecting context cancellation.FetchAllOrgDeviceshelper: Fetches all org devices following pagination links, returning the full[]OrgDeviceslice and total count from metadata. Follows the same pattern as the existingFetchOrgDevicePartNumbers.Context
These changes come from building axm2snipe, which syncs ABM/ASM devices into Snipe-IT. We discovered Apple's rate limits are quite low (~5 req/sec) and the backoff is essential for fetching AppleCare coverage for hundreds of devices.
Test plan
go build ./...passesgo vet ./...passesFetchAllOrgDevicescorrectly paginates through all devices🤖 Generated with Claude Code