Skip to content

feat: add retry with backoff for rate limits and FetchAllOrgDevices helper#3

Open
robbiet480 wants to merge 5 commits intozchee:mainfrom
CampusTech:axm2snipe-improvements
Open

feat: add retry with backoff for rate limits and FetchAllOrgDevices helper#3
robbiet480 wants to merge 5 commits intozchee:mainfrom
CampusTech:axm2snipe-improvements

Conversation

@robbiet480
Copy link

Summary

  • Retry with exponential backoff for HTTP 429: Apple's ABM API has unpublished rate limits and returns 429 when exceeded. doJSONRequest now automatically retries with exponential backoff (1s, 2s, 4s, 8s, 16s) up to 5 times on 429 responses, respecting context cancellation.
  • FetchAllOrgDevices helper: Fetches all org devices following pagination links, returning the full []OrgDevice slice and total count from metadata. Follows the same pattern as the existing FetchOrgDevicePartNumbers.

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 ./... passes
  • go vet ./... passes
  • Verified retry behavior against live ABM API (429 responses handled correctly with backoff)
  • Verified FetchAllOrgDevices correctly paginates through all devices

🤖 Generated with Claude Code

…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>
@gabrielsroka
Copy link

gabrielsroka commented Mar 5, 2026

not sure exponential backoff is necessary/good

response has a retry-after header. here's my Python code

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

@robbiet480
Copy link
Author

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>
@robbiet480
Copy link
Author

Added a commit to respect the Retry-After header on 429 responses.

Apple's ABM API returns Retry-After in seconds — observed values:

  • Device listing (pagination): Retry-After: 60
  • AppleCare coverage endpoint: Retry-After: 1

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>
@robbiet480
Copy link
Author

Added proactive rate limiting via a rateLimitTransport using golang.org/x/time/rate.

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 oauth2.Transport) so it covers both doJSONRequest and PageIterator calls, while not throttling OAuth token refreshes.

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 Retry-After handling from the previous commit is kept as a safety net.

robbiet480 and others added 2 commits March 5, 2026 15:50
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>
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

Successfully merging this pull request may close these issues.

2 participants