Private/Invoke-IslGraphRequest.ps1
|
function Invoke-IslGraphRequest { <# .SYNOPSIS Calls Microsoft Graph through the caller's Microsoft.Graph.Authentication session. .DESCRIPTION A thin wrapper over Invoke-MgGraphRequest: one place that checks the Graph module is loaded and connected, follows @odata.nextLink when -All is given, retries a throttled or unavailable answer, and is the single seam the unit tests mock. The module does not depend on Microsoft.Graph.Authentication; the caller installs it and runs Connect-MgGraph with DeviceManagementConfiguration.Read.All, DeviceManagementApps.Read.All and DeviceManagementScripts.Read.All (plus GroupMember.Read.All when assignments are inspected) before Test-IntuneDeployedScript is used. Graph answers 429 when a client is throttled and 503 or 504 when a service is briefly unavailable, with a Retry-After header on the first. A request that gets one of those is sent again up to three times, after the Retry-After seconds when the header is there and after 2, 4 and 8 seconds when it is not; the fourth failure is thrown as it came. A tenant command makes two requests per policy, so a pre-flight over a few hundred policies meets the throttle without this. .PARAMETER Uri The request URI, absolute or relative to the Graph root, e.g. /beta/deviceManagement/deviceHealthScripts. .PARAMETER Method GET (default) or POST. .PARAMETER Body The request body for a POST. .PARAMETER Headers Request headers, e.g. @{ ConsistencyLevel = 'eventual' } for a $count query. .PARAMETER All Follow @odata.nextLink and return every item of the collection instead of the first page. .EXAMPLE Invoke-IslGraphRequest -Uri '/beta/deviceManagement/deviceHealthScripts?$select=id,displayName' -All Every remediation in the tenant, id and name only. .EXAMPLE $countSplat = @{ Headers = @{ ConsistencyLevel = 'eventual' } } Invoke-IslGraphRequest -Uri '/v1.0/groups/{id}/members/microsoft.graph.device/$count' @countSplat The number of devices among the group's members, as text. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Uri, [ValidateSet('GET', 'POST')] [string]$Method = 'GET', [hashtable]$Body, [hashtable]$Headers, [switch]$All ) if (-not (Get-Command -Name Invoke-MgGraphRequest -ErrorAction SilentlyContinue)) { throw ('Microsoft.Graph.Authentication is not loaded. Install-PSResource ' + 'Microsoft.Graph.Authentication, then Connect-MgGraph -Scopes ' + 'DeviceManagementConfiguration.Read.All, DeviceManagementApps.Read.All, ' + 'DeviceManagementScripts.Read.All, GroupMember.Read.All') } $requestSplat = @{ Method = $Method; Uri = $Uri; ErrorAction = 'Stop' } if ($Body) { $requestSplat.Body = $Body } if ($Headers) { $requestSplat.Headers = $Headers } # How long to wait before sending a failed request again, or nothing when the failure is not # one that passes. Invoke-MgGraphRequest's exception carries the status code and the response; # both are read by name so a stand-in exception with the same members serves in the tests function Get-RetryDelay { param($ErrorRecord, [int]$Attempt) $exception = $ErrorRecord.Exception $status = 0 if ($exception.PSObject.Properties['StatusCode'] -and $exception.StatusCode) { $status = [int]$exception.StatusCode } $response = if ($exception.PSObject.Properties['Response']) { $exception.Response } else { $null } if (-not $status -and $response) { $status = [int]$response.StatusCode } if ($status -notin 429, 503, 504) { return $null } $delay = 0 $retryAfter = if ($response) { $response.Headers.RetryAfter } else { $null } if ($retryAfter) { if ($null -ne $retryAfter.Delta) { $delay = [Math]::Ceiling($retryAfter.Delta.TotalSeconds) } elseif ($null -ne $retryAfter.Date) { $delay = [Math]::Ceiling(($retryAfter.Date.UtcDateTime - [datetime]::UtcNow).TotalSeconds) } } if ($delay -lt 1) { $delay = [Math]::Pow(2, $Attempt) } [int][Math]::Min($delay, 60) } function Invoke-Once { param([hashtable]$Splat) $attempt = 0 while ($true) { try { return Invoke-MgGraphRequest @Splat } catch { $attempt++ $delay = if ($attempt -le 3) { Get-RetryDelay -ErrorRecord $_ -Attempt $attempt } else { $null } if ($null -eq $delay) { throw } Write-Verbose ("Graph answered $($_.Exception.Message); retry $attempt of 3 in $delay s: " + $Splat.Uri) Start-Sleep -Seconds $delay } } } if (-not $All) { return Invoke-Once -Splat $requestSplat } $items = [System.Collections.Generic.List[object]]::new() $next = $Uri while ($next) { $requestSplat.Uri = $next $page = Invoke-Once -Splat $requestSplat if ($page.value) { $items.AddRange([object[]]$page.value) } $next = $page.'@odata.nextLink' } $items.ToArray() } |