Private/Invoke-MgGraphRequestWithRetry.ps1
|
function Invoke-MgGraphRequestWithRetry { <# .SYNOPSIS Wraps Invoke-MgGraphRequest with automatic retry on HTTP 429 (throttling). .DESCRIPTION Manual pagination loops over large tenants can be throttled by Graph. Without a retry, the catch block typically logs a warning and continues, silently returning incomplete results. This helper retries on 429, honoring the Retry-After header when present and otherwise backing off exponentially (capped at 60s). Any non-429 error is rethrown unchanged. .PARAMETER Uri The Graph request URI. .PARAMETER Method HTTP method (default GET). .PARAMETER Body Optional request body. .PARAMETER MaxRetries Maximum number of retry attempts on 429 (default 5). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Uri, [Parameter()] [string]$Method = 'GET', [Parameter()] $Body, [Parameter()] [int]$MaxRetries = 5 ) $attempt = 0 while ($true) { try { $params = @{ Method = $Method; Uri = $Uri; ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { $params['Body'] = $Body } return Invoke-MgGraphRequest @params } catch { # Best-effort status-code extraction across SDK/exception shapes. $statusCode = $null foreach ($candidate in @( { [int]$_.Exception.Response.StatusCode }, { [int]$_.Exception.ResponseStatusCode }, { [int]$_.Exception.StatusCode } )) { try { $value = & $candidate if ($value) { $statusCode = $value; break } } catch { } } if ($statusCode -eq 429 -and $attempt -lt $MaxRetries) { $attempt++ $retryAfter = 0 try { $retryAfter = [int]$_.Exception.Response.Headers.RetryAfter.Delta.TotalSeconds } catch { } if (-not $retryAfter -or $retryAfter -lt 1) { $retryAfter = [int][math]::Min([math]::Pow(2, $attempt), 60) } Write-Warning "Microsoft Graph throttled the request (429). Retry $attempt/$MaxRetries after $retryAfter second(s)." Start-Sleep -Seconds $retryAfter continue } throw } } } |