Private/Invoke-MgGraphRequestWithRetry.ps1
|
function Invoke-MgGraphRequestWithRetry { <# .SYNOPSIS Wrapper around Invoke-MgGraphRequest with automatic 429 throttle handling. .DESCRIPTION Retries the request on HTTP 429 (Too Many Requests) using the Retry-After header value or exponential backoff (4s, 8s, 16s). Passes all parameters through to Invoke-MgGraphRequest via splatting. .PARAMETER Parameters Hashtable of parameters to pass to Invoke-MgGraphRequest (e.g. Method, Uri). .PARAMETER MaxRetries Maximum number of retry attempts after throttling. Default: 3. .EXAMPLE Invoke-MgGraphRequestWithRetry -Parameters @{ Method = 'GET'; Uri = $Uri } #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$Parameters, [Parameter(Mandatory = $false)] [ValidateRange(1, 10)] [int]$MaxRetries = 3 ) process { [int]$Attempt = 0 while ($true) { try { return Invoke-MgGraphRequest @Parameters } catch { $Attempt++ # Prefer the actual HTTP status code over string matching (avoids false positives # if "429" appears elsewhere in the error message, e.g. in a device ID or path). [int]$HttpStatus = 0 [int]$WaitSeconds = [Math]::Pow(2, $Attempt) * 2 if ($_.Exception.PSObject.Properties['Response'] -and $null -ne $_.Exception.Response) { $HttpStatus = [int]$_.Exception.Response.StatusCode # Honour Retry-After header; fall back to exponential backoff (4, 8, 16 seconds) [string]$RetryAfterHeader = $_.Exception.Response.Headers['Retry-After'] [int]$ParsedSeconds = 0 if (-not [string]::IsNullOrEmpty($RetryAfterHeader) -and [int]::TryParse($RetryAfterHeader, [ref]$ParsedSeconds)) { $WaitSeconds = $ParsedSeconds } } # Fall back to string matching only when no Response object is available [bool]$IsThrottled = ($HttpStatus -eq 429) -or ($HttpStatus -eq 0 -and "$_" -match '\b429\b') if ($Attempt -gt $MaxRetries -or -not $IsThrottled) { throw } Write-Warning "Graph API throttled (429). Waiting $WaitSeconds seconds before retry $Attempt/$MaxRetries..." Start-Sleep -Seconds $WaitSeconds } } } } |