Private/Graph/Invoke-HydrationGraphRequest.ps1
|
function Invoke-HydrationGraphRequest { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$Method, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Uri, [Parameter()] [AllowNull()] [object]$Body, [Parameter()] [string]$ContentType = 'application/json', [Parameter()] [int]$MaxRetries = 3, [Parameter()] [int]$RetryDelaySeconds = 2 ) function Resolve-GraphUri { param( [Parameter(Mandatory)] [string]$RawUri ) if ([string]::IsNullOrWhiteSpace($RawUri)) { return $RawUri } if ($RawUri -match '^https?://') { $graphEndpoint = if ($script:GraphEndpoint) { $script:GraphEndpoint.TrimEnd('/') } else { 'https://graph.microsoft.com' } if ($RawUri.StartsWith($graphEndpoint, [System.StringComparison]::OrdinalIgnoreCase)) { return ($RawUri.Substring($graphEndpoint.Length)).TrimStart('/') } return $RawUri } return $RawUri.TrimStart('/') } function Get-GraphUriForLogging { param( [Parameter(Mandatory)] [string]$RawUri ) $uriSegments = $RawUri -split '\?', 2 if ($uriSegments.Count -eq 1) { return $uriSegments[0] } $queryKeys = foreach ($pair in ($uriSegments[1] -split '&')) { if ([string]::IsNullOrWhiteSpace($pair)) { continue } $queryPair = $pair -split '=', 2 '{0}=...' -f $queryPair[0] } return '{0}?{1}' -f $uriSegments[0], ($queryKeys -join '&') } function Get-GraphBodySummary { param( [Parameter()] [AllowNull()] [object]$Value, [Parameter()] [string]$ResolvedContentType ) if ($null -eq $Value) { return 'Body=None' } if ($Value -is [string]) { return "Body=String(Length=$($Value.Length), ContentType='$ResolvedContentType')" } if ($Value -is [System.Collections.IDictionary]) { $bodyKeys = @($Value.Keys) return "Body=Dictionary(KeyCount=$($bodyKeys.Count), Keys='$((@($bodyKeys | Select-Object -First 8)) -join ', ')')" } $propertyNames = @($Value.PSObject.Properties.Name) if ($propertyNames.Count -gt 0) { return "Body=Object(Type='$($Value.GetType().FullName)', PropertyCount=$($propertyNames.Count), Properties='$((@($propertyNames | Select-Object -First 8)) -join ', ')')" } return "Body=Object(Type='$($Value.GetType().FullName)')" } function Get-RetryDelay { param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [Parameter(Mandatory)] [int]$Attempt, [Parameter(Mandatory)] [int]$BaseDelaySeconds ) $headerCandidates = @( $ErrorRecord.Exception.ResponseHeaders, $ErrorRecord.Exception.Response.Headers, $ErrorRecord.Exception.Headers ) foreach ($headers in $headerCandidates) { if (-not $headers) { continue } foreach ($headerName in @('Retry-After', 'retry-after')) { $headerValue = $null if ($headers -is [System.Collections.IDictionary] -and $headers.Contains($headerName)) { $headerValue = $headers[$headerName] } elseif ($headers.PSObject -and $headers.PSObject.Properties[$headerName]) { $headerValue = $headers.$headerName } $parsedValue = 0 if ([int]::TryParse([string]$headerValue, [ref]$parsedValue) -and $parsedValue -gt 0) { return $parsedValue } } } return $BaseDelaySeconds * [Math]::Pow(2, $Attempt - 1) } function Resolve-GraphErrorRecord { param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) $candidateRecords = @( $ErrorRecord, $ErrorRecord.Exception.ErrorRecord, $ErrorRecord.Exception.InnerException.ErrorRecord ) foreach ($candidateRecord in $candidateRecords) { if ($null -eq $candidateRecord) { continue } $candidateStatusCode = Get-GraphStatusCode -ErrorRecord $candidateRecord if ($candidateStatusCode) { return $candidateRecord } } return $ErrorRecord } $resolvedUri = Resolve-GraphUri -RawUri $Uri $uriForLogging = Get-GraphUriForLogging -RawUri $resolvedUri $invokeParams = @{ Method = $Method Uri = $resolvedUri ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { if ($Body -is [string]) { $invokeParams['Body'] = $Body if ($ContentType) { $invokeParams['ContentType'] = $ContentType } } else { $invokeParams['Body'] = $Body } } $bodySummary = Get-GraphBodySummary -Value $Body -ResolvedContentType $ContentType Write-Debug "Invoking Graph request Method='$Method', Uri='$uriForLogging', $bodySummary." for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) { try { return Invoke-MgGraphRequest @invokeParams } catch { $graphErrorRecord = Resolve-GraphErrorRecord -ErrorRecord $_ $statusCode = Get-GraphStatusCode -ErrorRecord $graphErrorRecord $isRetryableStatusCode = $null -ne $statusCode -and ($statusCode -eq 429 -or ($statusCode -ge 500 -and $statusCode -lt 600)) Write-Debug "Graph request failed Method='$Method', Uri='$uriForLogging', StatusCode='$statusCode', Attempt=$attempt/$MaxRetries, Retryable=$isRetryableStatusCode, Error='$($_.Exception.Message)'." if (-not $isRetryableStatusCode -or $attempt -eq $MaxRetries) { throw } $delaySeconds = Get-RetryDelay -ErrorRecord $graphErrorRecord -Attempt $attempt -BaseDelaySeconds $RetryDelaySeconds Write-Verbose "Graph request failed with HTTP $statusCode. Retrying in $delaySeconds second(s) (attempt $attempt of $MaxRetries)." Start-Sleep -Seconds $delaySeconds } } } |