Modules/Private/Main/Invoke-AZTIGraphRequest.ps1
|
<#
.Synopsis Execute a Microsoft Graph REST API request with automatic pagination and throttle handling. .DESCRIPTION Wrapper around Invoke-RestMethod for Microsoft Graph API calls. Automatically: - Obtains a bearer token via Get-AZSCGraphToken - Builds the full URL from a relative path (e.g. /v1.0/users) - Follows @odata.nextLink for multi-page responses - Handles HTTP 429 (Too Many Requests) with Retry-After header - Returns the aggregated .value array (or raw response for single-object queries) NO Microsoft.Graph SDK dependency — uses Invoke-RestMethod directly. .PARAMETER Uri Relative Graph API path, e.g. '/v1.0/users' or '/beta/identity/conditionalAccess/policies'. A full URL (https://graph.microsoft.com/...) is also accepted. .PARAMETER Method HTTP method. Default: GET. .PARAMETER Body Request body for POST/PATCH/PUT requests. Will be serialized to JSON if a hashtable/PSObject. .PARAMETER SinglePage Do not follow @odata.nextLink — return only the first page of results. .PARAMETER MaxRetries Maximum number of retries for transient errors (429, 5xx). Default: 5. .OUTPUTS [PSObject[]] Aggregated .value array, or the raw response for single-object endpoints. .LINK https://github.com/thisismydemo/azure-scout .COMPONENT This PowerShell Module is part of Azure Scout (AZSC) .NOTES Version: 1.0.0 Authors: thisismydemo #> function Invoke-AZSCGraphRequest { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Uri, [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')] [string]$Method = 'GET', [object]$Body, [switch]$SinglePage, [int]$MaxRetries = 5 ) $baseUrl = 'https://graph.microsoft.com' # Normalise URI — accept both relative ("/v1.0/users") and absolute URLs if ($Uri -notmatch '^https?://') { $fullUri = "$baseUrl$Uri" } else { $fullUri = $Uri } Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Graph $Method $fullUri") $allResults = [System.Collections.Generic.List[object]]::new() $currentUri = $fullUri do { $headers = Get-AZSCGraphToken $requestParams = @{ Uri = $currentUri Method = $Method Headers = $headers ErrorAction = 'Stop' } if ($Body) { if ($Body -is [string]) { $requestParams['Body'] = $Body } else { $requestParams['Body'] = $Body | ConvertTo-Json -Depth 20 } $requestParams['ContentType'] = 'application/json' } $response = $null $retryCount = 0 while ($retryCount -le $MaxRetries) { try { $response = Invoke-RestMethod @requestParams break } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } # Handle 429 (throttled) and 5xx (transient server errors) if ($statusCode -eq 429 -or ($statusCode -ge 500 -and $statusCode -lt 600)) { $retryCount++ if ($retryCount -gt $MaxRetries) { Write-Warning "Graph API request failed after $MaxRetries retries: $($_.Exception.Message)" throw } # Use Retry-After header if present, otherwise exponential backoff $retryAfter = $null if ($_.Exception.Response.Headers) { try { $retryAfterValues = $_.Exception.Response.Headers.GetValues('Retry-After') if ($retryAfterValues) { $retryAfter = [int]$retryAfterValues[0] } } catch { # Header not present — use backoff } } if (-not $retryAfter -or $retryAfter -lt 1) { $retryAfter = [math]::Pow(2, $retryCount) } Write-Warning "Graph API returned $statusCode. Retrying in $retryAfter seconds (attempt $retryCount/$MaxRetries)..." Start-Sleep -Seconds $retryAfter # Refresh token in case it expired during wait $headers = Get-AZSCGraphToken $requestParams['Headers'] = $headers } else { # Non-retryable error — propagate Write-Warning "Graph API request failed: $($_.Exception.Message)" throw } } } # Collect results if ($null -ne $response) { if ($response.PSObject.Properties.Name -contains 'value') { # Collection endpoint — accumulate .value items foreach ($item in $response.value) { $allResults.Add($item) } } else { # Single-object endpoint — return as-is return $response } } # Pagination $currentUri = $null if (-not $SinglePage -and $response.PSObject.Properties.Name -contains '@odata.nextLink') { $currentUri = $response.'@odata.nextLink' Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Following nextLink (collected $($allResults.Count) items so far)") } } while ($currentUri) Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Graph request complete. Total items: $($allResults.Count)") return $allResults.ToArray() } |