M365cde.Invoke-GraphRequestPaged.psm1
|
function Invoke-GraphRequestPaged { <# .SYNOPSIS Retrieves all pages of a Microsoft Graph GET request. .DESCRIPTION Calls Invoke-MgGraphRequest (Microsoft.Graph.Authentication) with the given URI and follows '@odata.nextLink' until all pages are retrieved. Throttled or temporarily unavailable responses (HTTP 429, 503, 504) are retried, honoring the 'Retry-After' header and falling back to exponential backoff. Each item of the 'value' collection is written to the pipeline; responses without a 'value' property (single objects) are written as they are. An active connection to Microsoft Graph (Connect-MgGraph) with sufficient permissions is required. Works in scripts and Azure Automation runbooks. .PARAMETER Uri The Microsoft Graph URI to query, relative (e.g. '/v1.0/users') or absolute (e.g. 'https://graph.microsoft.com/v1.0/users'). .PARAMETER Headers Optional additional request headers, e.g. @{ ConsistencyLevel = 'eventual' } for advanced queries. .PARAMETER MaxRetries Maximum number of retries per page for HTTP 429, 503 and 504. Default is 5. .PARAMETER AsHashtable Returns the raw hashtables of Invoke-MgGraphRequest instead of PSCustomObjects. By default results are converted (recursively) to PSCustomObjects so that Format-Table, Select-Object, Export-Csv etc. work as expected. .EXAMPLE Invoke-GraphRequestPaged -Uri 'https://graph.microsoft.com/v1.0/users?$select=displayName,userPrincipalName' Returns all users. .EXAMPLE Invoke-GraphRequestPaged -Uri '/v1.0/users?$count=true&$filter=startsWith(displayName,''A'')' -Headers @{ ConsistencyLevel = 'eventual' } Returns all matching users using an advanced query. #> [CmdletBinding()] [OutputType([object])] param ( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$Uri, [System.Collections.IDictionary]$Headers, [ValidateRange(0, 20)] [int]$MaxRetries = 5, [switch]$AsHashtable ) if (-not (Get-MgContext)) { throw 'Not connected to Microsoft Graph. Run Connect-MgGraph first.' } $nextUri = $Uri while ($nextUri) { $params = @{ Method = 'GET'; Uri = $nextUri; ErrorAction = 'Stop' } if ($Headers) { $params.Headers = $Headers } $attempt = 0 while ($true) { try { $response = Invoke-MgGraphRequest @params break } catch { $status = Get-GraphErrorStatusCode -ErrorRecord $_ if ($status -in 429, 503, 504 -and $attempt -lt $MaxRetries) { $attempt++ $wait = Get-GraphRetryAfterSeconds -ErrorRecord $_ if (-not $wait) { $wait = [math]::Pow(2, $attempt) } Write-Verbose "HTTP $status - retry $attempt/$MaxRetries in $wait s." Start-Sleep -Seconds $wait } else { throw } } } if ($response -is [System.Collections.IDictionary] -and $response.Contains('value')) { # Enumerate explicitly: an empty page must not emit $null. foreach ($item in $response['value']) { if ($AsHashtable) { $item } else { ConvertTo-GraphObject -InputObject $item } } } elseif ($null -ne $response) { # single object if ($AsHashtable) { $response } else { ConvertTo-GraphObject -InputObject $response } } $nextUri = if ($response -is [System.Collections.IDictionary]) { $response['@odata.nextLink'] } } } function ConvertTo-GraphObject { [CmdletBinding()] param ($InputObject) if ($InputObject -is [System.Collections.IDictionary]) { $ordered = [ordered]@{} foreach ($key in $InputObject.Keys) { $ordered[[string]$key] = ConvertTo-GraphObject -InputObject $InputObject[$key] } [pscustomobject]$ordered } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { # Keep arrays as arrays (the comma prevents unrolling of single/empty arrays). , @(foreach ($element in $InputObject) { ConvertTo-GraphObject -InputObject $element }) } else { $InputObject } } function Get-GraphErrorStatusCode { [CmdletBinding()] [OutputType([int])] param ([System.Management.Automation.ErrorRecord]$ErrorRecord) $webResponse = $ErrorRecord.Exception.Response if ($webResponse -and $null -ne $webResponse.StatusCode) { return [int]$webResponse.StatusCode } return 0 } function Get-GraphRetryAfterSeconds { [CmdletBinding()] [OutputType([int])] param ([System.Management.Automation.ErrorRecord]$ErrorRecord) $webResponse = $ErrorRecord.Exception.Response if (-not $webResponse -or -not $webResponse.Headers) { return 0 } $value = $null $headers = $webResponse.Headers if ($headers -is [System.Net.Http.Headers.HttpHeaders]) { # PowerShell 7 (HttpResponseMessage) $values = $null if ($headers.TryGetValues('Retry-After', [ref]$values)) { $value = $values | Select-Object -First 1 } } else { # Windows PowerShell 5.1 (WebHeaderCollection) $value = $headers['Retry-After'] } $seconds = 0 if ([int]::TryParse([string]$value, [ref]$seconds) -and $seconds -gt 0) { return [math]::Min($seconds, 300) } return 0 } Export-ModuleMember -Function Invoke-GraphRequestPaged |