Private/Invoke-GraphRequestWithRetry.ps1
|
function Invoke-GraphRequestWithRetry { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('GET','POST','PATCH','DELETE')][string]$Method, [Parameter(Mandatory)][string]$Uri, $Body = $null, [hashtable]$Headers = @{}, [ValidateRange(1,2147483647)][int]$MaxRetries = 5, [string]$OutputFilePath ) $requestUri = Resolve-UTCMGraphUri -Uri $Uri $logUri = ([uri]$requestUri).GetLeftPart([UriPartial]::Path) $request = @{ Method = $Method; Uri = $requestUri; ErrorAction = 'Stop' } if ($null -ne $Body) { $request.Body = $Body $request.ContentType = 'application/json' } if ($Headers.Count -gt 0) { $request.Headers = $Headers } if ($PSBoundParameters.ContainsKey('OutputFilePath')) { $request.OutputFilePath = $OutputFilePath } $attempt = 0 while ($attempt -lt $MaxRetries) { try { $attempt++ return Invoke-MgGraphRequest @request } catch { $ex = $_.Exception $message = $ex.Message # ── Try to extract the Graph API error detail ── # Invoke-MgGraphRequest puts the response body in multiple places # depending on SDK version. We check them all. $graphErrorDetail = $null try { # 1. PowerShell ErrorRecord.ErrorDetails.Message (most reliable) if (-not $graphErrorDetail -and $_.ErrorDetails -and $_.ErrorDetails.Message) { $graphErrorDetail = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue } # 2. Exception.Response (HttpResponseMessage) — read body stream if (-not $graphErrorDetail -and $ex.Response) { $httpResp = $ex.Response if ($httpResp.Content) { $bodyText = $httpResp.Content.ReadAsStringAsync().GetAwaiter().GetResult() if ($bodyText) { $graphErrorDetail = $bodyText | ConvertFrom-Json -ErrorAction SilentlyContinue } } } # 3. SDK v2+ may expose .ResponseBody directly if (-not $graphErrorDetail -and $ex.PSObject.Properties.Name -contains 'ResponseBody') { $graphErrorDetail = $ex.ResponseBody | ConvertFrom-Json -ErrorAction SilentlyContinue } # 4. InnerException chain if (-not $graphErrorDetail -and $ex.InnerException) { $inner = $ex.InnerException if ($inner.PSObject.Properties.Name -contains 'ResponseBody') { $graphErrorDetail = $inner.ResponseBody | ConvertFrom-Json -ErrorAction SilentlyContinue } } } catch { <# ignore extraction failures #> } if ($graphErrorDetail) { $detailMsg = $graphErrorDetail.error.message ?? ($graphErrorDetail | ConvertTo-Json -Depth 5 -Compress) $detailCode = $graphErrorDetail.error.code ?? 'unknown' $message = "$message [Graph error code=$detailCode detail=$detailMsg]" } else { # Last resort: dump raw ErrorDetails string if it exists but wasn't valid JSON if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $message = "$message [Raw error detail: $($_.ErrorDetails.Message)]" } } # ── Determine HTTP status code ── $statusCode = 0 try { if ($ex.PSObject.Properties.Name -contains 'ResponseStatusCode') { $statusCode = [int]$ex.ResponseStatusCode } elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode) { $statusCode = [int]$_.Exception.Response.StatusCode } } catch { <# ignore #> } if ($statusCode -ge 400 -and $statusCode -lt 500 -and $statusCode -ne 429) { throw "Graph request failed with HTTP $statusCode. Method=$Method Uri=$logUri Error: $message" } if ($attempt -ge $MaxRetries) { throw "Graph request failed after $MaxRetries attempts (HTTP $statusCode). Method=$Method Uri=$logUri Error: $message" } # ── Check for throttling (429/503) ── if ($statusCode -in @(429, 503)) { $retryAfter = 0 try { $retryAfter = [int]$_.Exception.Response.Headers['Retry-After'] } catch { $retryAfter = 0 } if ($retryAfter -le 0) { $retryAfter = [math]::Min(60, [math]::Pow(2, $attempt)) } Write-Log -Message "Graph throttled/temporarily unavailable (attempt $attempt). Retrying in $retryAfter sec..." -Color Yellow Start-Sleep -Seconds $retryAfter continue } # ── Server/transient error — retry with exponential backoff ── $delay = [math]::Min(30, [math]::Pow(2, $attempt)) Write-Log -Message "Transient error (attempt $attempt): $message. Retrying in $delay sec..." -Color Yellow Start-Sleep -Seconds $delay } } } |