Private/Graph/Invoke-IntuneDiffRequest.ps1
|
# Copyright (c) 2026 Sandy Zeng. All rights reserved. # Source-available. All rights reserved. See LICENSE file. <# Invoke-IntuneDiffRequest.ps1 — REST wrapper for Microsoft Graph with pagination, 429 retry, and silent token refresh on 401. Uses Invoke-RestMethod directly so the module has no dependency on Microsoft.Graph.Authentication. Author: Sandy Zeng Project: IntuneDiff Version History: 1.0.0 Initial release. 2.0.0 Rewritten to use Invoke-RestMethod with a Bearer token — no Microsoft.Graph SDK. #> function Invoke-IntuneDiffRequest { <# .SYNOPSIS Sends a Microsoft Graph REST request with pagination and 429 retry. .PARAMETER Method HTTP method. Defaults to GET. .PARAMETER Uri Absolute (https://graph.microsoft.com/...) or relative (/beta/...) Graph URI. .PARAMETER Body Optional request body (hashtable) for POST/PATCH. .PARAMETER All For GET requests, follow @odata.nextLink until exhausted and return a hashtable whose `value` property contains every page concatenated. .PARAMETER TimeoutSec Per-request timeout. Defaults to 30 seconds. #> [CmdletBinding()] param( [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')] [string]$Method = 'GET', [Parameter(Mandatory)] [string]$Uri, [object]$Body, [switch]$All, [int]$TimeoutSec = 30 ) Write-IDLog "Graph $Method $Uri" $maxRetries = 5 $tokenRefreshed = $false # Normalize relative URIs to absolute if ($Uri -notmatch '^https?://') { if ($Uri.StartsWith('/')) { $Uri = "https://graph.microsoft.com$Uri" } else { $Uri = "https://graph.microsoft.com/$Uri" } } $invokeOnce = { param([string]$method, [string]$uri, [object]$bodyJson, [int]$timeoutSec) $headers = @{ Authorization = "Bearer $(Get-IDAccessToken)" Accept = 'application/json' } $params = @{ Method = $method Uri = $uri Headers = $headers TimeoutSec = $timeoutSec ErrorAction = 'Stop' } if ($bodyJson) { $params['Body'] = $bodyJson $params['ContentType'] = 'application/json' } return Invoke-RestMethod @params } $jsonBody = $null if ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body) { $jsonBody = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 20 -Compress } } if ($Method -eq 'GET' -and $All) { $aggregated = @() $firstResponse = $null $currentUri = $Uri while ($currentUri) { $attempt = 0 $response = $null while ($true) { try { $response = & $invokeOnce 'GET' $currentUri $null $TimeoutSec break } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } if ($statusCode -eq 429 -and $attempt -lt $maxRetries) { $retryAfter = 2 try { $headerValue = $_.Exception.Response.Headers['Retry-After'] if ($headerValue) { $retryAfter = [int]$headerValue } } catch { } Start-Sleep -Seconds $retryAfter $attempt++ continue } if ($statusCode -eq 401 -and -not $tokenRefreshed) { Write-IDLog '401 Unauthorized — forcing token refresh and retrying' try { Get-IDAccessToken -ForceRefresh | Out-Null } catch { throw } $tokenRefreshed = $true continue } throw } } if (-not $firstResponse) { $firstResponse = $response } if ($response.value) { $aggregated += $response.value } $nextLink = $response.'@odata.nextLink' if ($nextLink -and $nextLink -notmatch '^https://graph\.microsoft\.com/') { Write-Warning "IntuneDiff: Ignoring untrusted @odata.nextLink: $nextLink" $nextLink = $null } $currentUri = $nextLink } if ($firstResponse -is [hashtable]) { $firstResponse['value'] = $aggregated return $firstResponse } return @{ value = $aggregated } } # Single-shot (non-paginated) request $attempt = 0 while ($true) { try { return (& $invokeOnce $Method $Uri $jsonBody $TimeoutSec) } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } if ($statusCode -eq 429 -and $attempt -lt $maxRetries) { $retryAfter = 2 try { $headerValue = $_.Exception.Response.Headers['Retry-After'] if ($headerValue) { $retryAfter = [int]$headerValue } } catch { } Start-Sleep -Seconds $retryAfter $attempt++ continue } if ($statusCode -eq 401 -and -not $tokenRefreshed) { Write-IDLog '401 Unauthorized — forcing token refresh and retrying' try { Get-IDAccessToken -ForceRefresh | Out-Null } catch { throw } $tokenRefreshed = $true continue } throw } } } |