Private/Invoke-AzRestWithRetry.ps1
|
function Invoke-AzRestWithRetry { <# .SYNOPSIS Wraps Invoke-RestMethod with retry logic for transient Azure API failures. .DESCRIPTION Retries on HTTP 429 (throttled, respects Retry-After header) and transient 5xx server errors. Uses exponential backoff with a configurable maximum number of attempts. Non-retryable errors are re-thrown immediately. .OUTPUTS The response object from Invoke-RestMethod. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][hashtable]$Headers, [string]$Method = 'Get', [object]$Body, [int]$MaxRetries = 3, [int]$BaseDelaySeconds = 2 ) $attempt = 0 while ($true) { $attempt++ try { $splat = @{ Uri = $Uri Headers = $Headers Method = $Method ErrorAction = 'Stop' } if ($null -ne $Body) { $splat['Body'] = $Body } return Invoke-RestMethod @splat } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } $retryable = $statusCode -eq 429 -or ($statusCode -ge 500 -and $statusCode -le 599) if (-not $retryable -or $attempt -gt $MaxRetries) { throw } # Determine wait time: use Retry-After header for 429, otherwise exponential backoff $waitSeconds = $BaseDelaySeconds * [math]::Pow(2, $attempt - 1) if ($statusCode -eq 429 -and $_.Exception.Response.Headers) { $retryAfter = $null try { $retryAfter = $_.Exception.Response.Headers | Where-Object Key -eq 'Retry-After' | Select-Object -ExpandProperty Value -First 1 } catch { Write-Verbose "Could not parse Retry-After header: $_" } if ($retryAfter -and [int]::TryParse($retryAfter, [ref]$null)) { $waitSeconds = [int]$retryAfter } } Write-Warning "Request to $Uri failed (HTTP $statusCode). Retry $attempt of $MaxRetries in ${waitSeconds}s." Start-Sleep -Seconds $waitSeconds } } } |