Private/Invoke-CaEvaluateRequest.ps1
|
function Invoke-CaEvaluateRequest { <# .SYNOPSIS Sends one evaluate request, retrying a throttle or a transient failure .DESCRIPTION The single point where this module talks to anything. Isolating it is what keeps the rest of the module a pure transform with no dependencies and no connection, and what makes the whole thing testable without a tenant: the request handler is a parameter, so a test supplies a scriptblock and never touches the network. The default handler calls Invoke-MgGraphRequest, checked for at run time rather than declared with #Requires. A #Requires -Modules line is enforced whenever the file is loaded, including when Pester dot-sources it, which would make this module untestable on any host without the Graph SDK installed - and this module has no need of the SDK at all unless somebody actually sends a request. Backoff is exponential with the server's Retry-After taking precedence when it sends one. The delay doubles per attempt, so the default five attempts at two seconds spans about half a minute before giving up, which is long enough to ride out a throttle and short enough that a genuinely broken run does not sit there for an hour. .PARAMETER Body The request body from ConvertTo-CaEvaluateBody. .PARAMETER Uri The evaluate endpoint. .PARAMETER RequestHandler A scriptblock taking the body and the URI and returning the raw response. Defaults to Invoke-MgGraphRequest. .PARAMETER MaxRetry How many times to retry before giving up. .PARAMETER InitialBackoffSecond The first backoff, doubling per attempt. Zero disables waiting, which is what the tests use. .OUTPUTS The raw response, in whatever shape the handler returned. .EXAMPLE Invoke-CaEvaluateRequest -Body $body -RequestHandler { param($b, $u) $canned } .NOTES Author: Jeffrey Stuhr Version: 0.2.0 Last Updated: 2026-08-17 #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNull()] [hashtable]$Body, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Uri = 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate', [Parameter()] [scriptblock]$RequestHandler, [Parameter()] [ValidateRange(0, 20)] [int]$MaxRetry = 4, [Parameter()] [ValidateRange(0, 300)] [int]$InitialBackoffSecond = 2 ) if (-not $RequestHandler) { if (-not (Get-Command -Name 'Invoke-MgGraphRequest' -ErrorAction SilentlyContinue)) { throw ('Invoke-MgGraphRequest was not found. Install and import ' + 'Microsoft.Graph.Authentication and run Connect-MgGraph, or pass your own ' + '-RequestHandler.') } $RequestHandler = { param($RequestBody, $RequestUri) Invoke-MgGraphRequest -Method POST -Uri $RequestUri -OutputType Json ` -Body ($RequestBody | ConvertTo-Json -Depth 10) } } $attempt = 0 $backoff = $InitialBackoffSecond while ($true) { try { return & $RequestHandler $Body $Uri } catch { $directive = Get-CaRetryDirective -ErrorRecord $_ if (-not $directive.ShouldRetry -or $attempt -ge $MaxRetry) { throw } $attempt++ $wait = $backoff if ($null -ne $directive.RetryAfterSecond) { # The server's number beats anything invented here $wait = $directive.RetryAfterSecond } $status = if ($null -ne $directive.StatusCode) { $directive.StatusCode } else { 'transient failure' } Write-Verbose "Attempt $attempt of $MaxRetry after $status; waiting $wait second(s)" if ($wait -gt 0) { Start-Sleep -Seconds $wait } $backoff = $backoff * 2 } } } |