The Graph client previously failed the call on the first 429/503. The
request loop now retries these transient statuses before giving up.
Behavior:
- 429 and 503 are retried up to OUTLOOK_MAX_RETRIES times.
- Retry-After header is honored when Graph sends it, clamped to
MAX_RETRY_DELAY_MS.
- Otherwise exponential backoff with full jitter, same clamp.
- 401 still throws UNAUTHORIZED immediately (no retry, no point).
- Other 4xx (400/403/404, etc.) are NOT retried -- these are caller
errors, retrying just burns the rate-limit budget.
New config knobs, all env-overridable:
- OUTLOOK_MAX_RETRIES (default 3)
- OUTLOOK_BASE_RETRY_DELAY_MS (default 1000)
- OUTLOOK_MAX_RETRY_DELAY_MS (default 30000)
Implementation:
- Refactored the request into makeSingleRequest + optionsForRequest
helpers so the retry loop sits above them without duplicating the
response parsing.
- Empty-body success (202/204, e.g. sendMail, DELETE) is preserved.
- sleep and getRetryDelay are exported for the test.
Tests:
- tests/backoff.test.js: spins up a local HTTPS server, asserts
getRetryDelay honors Retry-After and clamps to MAX_RETRY_DELAY_MS,
and that callGraphAPI retries on 429 and fails after maxAttempts.