Private/Invoke-TBGraphRequest.ps1
|
function Invoke-TBGraphRequest { <# .SYNOPSIS Central wrapper for Microsoft Graph API calls with retry and error handling. .DESCRIPTION Wraps Invoke-MgGraphRequest with automatic retry on 429 (throttling) and 503 (service unavailable), verbose logging, and structured error parsing. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Uri, [Parameter()] [ValidateSet('GET', 'POST', 'PATCH', 'DELETE')] [string]$Method = 'GET', [Parameter()] [object]$Body, [Parameter()] [int]$MaxRetries = 3, [Parameter()] [hashtable]$Headers ) $null = Test-TBGraphConnection $attempt = 0 $baseDelay = 2 while ($true) { $attempt++ Write-TBLog -Message ('{0} {1} (attempt {2})' -f $Method, $Uri, $attempt) try { $params = @{ Uri = $Uri Method = $Method } if ($Body) { $params['Body'] = $Body | ConvertTo-Json -Depth 20 -Compress if (-not $params.ContainsKey('ContentType')) { $params['ContentType'] = 'application/json' } Write-TBLog -Message ('Request body: {0}' -f $params['Body']) -Level 'Verbose' } if ($Headers) { $params['Headers'] = $Headers } $response = Invoke-MgGraphRequest @params Write-TBLog -Message ('{0} {1} succeeded' -f $Method, $Uri) return $response } catch { # Try to capture the API response body from all available sources $responseBody = $null # Source 1: ErrorDetails.Message if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $responseBody = $_.ErrorDetails.Message Write-TBLog -Message ('ErrorDetails.Message: {0}' -f $responseBody) -Level 'Verbose' } # Source 2: Read from HttpResponseMessage content stream if (-not $responseBody) { try { if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response -and $_.Exception.Response.PSObject.Properties['Content'] -and $_.Exception.Response.Content) { $responseBody = $_.Exception.Response.Content.ReadAsStringAsync().GetAwaiter().GetResult() if ($responseBody) { Write-TBLog -Message ('Response body from stream: {0}' -f $responseBody) -Level 'Verbose' } } } catch { # Content stream may already be disposed } } # Source 3: Check TargetObject (some SDK versions store response here) if (-not $responseBody -and $_.TargetObject -is [string] -and $_.TargetObject -match '\{') { $responseBody = $_.TargetObject Write-TBLog -Message ('TargetObject: {0}' -f $responseBody) -Level 'Verbose' } Write-TBLog -Message ('Exception type: {0}' -f $_.Exception.GetType().FullName) -Level 'Verbose' $resolveParams = @{ ErrorRecord = $_ } if ($responseBody) { $resolveParams['ResponseBody'] = $responseBody } $parsedError = Resolve-TBErrorResponse @resolveParams $statusCode = $parsedError.StatusCode $isRetryable = ($statusCode -eq 429) -or ($statusCode -eq 503) -or ($statusCode -eq 504) if ($isRetryable -and ($attempt -lt $MaxRetries)) { # Calculate delay: use Retry-After header if available, otherwise exponential backoff $retryAfter = $null if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response.Headers -and $_.Exception.Response.Headers['Retry-After']) { $retryAfter = [int]$_.Exception.Response.Headers['Retry-After'][0] } if (-not $retryAfter) { $retryAfter = [math]::Pow($baseDelay, $attempt) } Write-TBLog -Message ('Request throttled or service unavailable (HTTP {0}). Retrying in {1}s...' -f $statusCode, $retryAfter) -Level 'Warning' Start-Sleep -Seconds $retryAfter } else { $errorMsg = 'Graph API request failed: [{0}] {1}' -f $parsedError.ErrorCode, $parsedError.Message if ($parsedError.RequestId) { $errorMsg += ' (Request ID: {0})' -f $parsedError.RequestId } Write-TBLog -Message $errorMsg -Level 'Error' throw $_ } } } } |