Private/EAI/Invoke-EAIRestMethod.ps1
|
class EAIException : System.Exception { EAIException([string]$message) : base($message) { } EAIException([string]$message, [System.Exception]$innerException) : base($message, $innerException) { } } function Format-EAIErrorMessage { [CmdletBinding()] param( [String]$ResponseBody, [Nullable[int]]$StatusCode ) if ([string]::IsNullOrWhiteSpace($ResponseBody)) { if ($StatusCode) { return "Edwin API request failed with HTTP status $StatusCode." } return 'Edwin API request failed.' } try { $parsed = $ResponseBody | ConvertFrom-Json -ErrorAction Stop $code = if ($null -ne $parsed.PSObject.Properties['code']) { $parsed.code } elseif ($null -ne $parsed.PSObject.Properties['status_code']) { $parsed.status_code } elseif ($StatusCode) { $StatusCode } else { 'Error' } $message = if ($null -ne $parsed.PSObject.Properties['message']) { [string]$parsed.message } else { $null } $traceId = if ($null -ne $parsed.PSObject.Properties['id'] -and -not [string]::IsNullOrWhiteSpace([string]$parsed.id)) { [string]$parsed.id } else { $null } $formattedMessage = $null if ($parsed.errors) { $details = if ($parsed.errors -is [System.Array]) { ($parsed.errors | ForEach-Object { [string]$_ }) -join '; ' } else { [string]$parsed.errors } if ($message) { $formattedMessage = "${code}: ${message} [$details]" } else { $formattedMessage = "${code}: $details" } } elseif ($message) { $formattedMessage = "${code}: ${message}" } if ($formattedMessage) { if ($traceId) { $formattedMessage = "$formattedMessage (trace id: $traceId)" } return $formattedMessage } } catch { return $ResponseBody } return $ResponseBody } function Resolve-EAIException { [CmdletBinding()] param( [Nullable[int]]$StatusCode, [String]$ResponseBody, [PSCustomObject]$ErrorContext ) $message = Format-EAIErrorMessage -ResponseBody $ResponseBody -StatusCode $StatusCode $result = [PSCustomObject]@{ Message = $message ErrorType = 'ClientError' ErrorId = 'EAI.ClientError' Category = [System.Management.Automation.ErrorCategory]::InvalidOperation ShouldRetry = $false } switch ($StatusCode) { 400 { if ($message -match 'Cannot evaluate query') { $result.ErrorType = 'QueryEvaluationError' $result.ErrorId = 'EAI.QueryEvaluationError' $result.Category = [System.Management.Automation.ErrorCategory]::InvalidData $hint = Format-EAIQueryEvaluationErrorHint -ErrorContext $ErrorContext if ($hint) { $result.Message = "$message$hint" } } } 401 { $result.ErrorType = 'AuthenticationError' $result.ErrorId = 'EAI.AuthenticationError' $result.Category = [System.Management.Automation.ErrorCategory]::AuthenticationError if ($message -notmatch 'Connect-EAIAccount') { $result.Message = "$message Verify your EdwinOrg, ClientId, and ClientSecret with Connect-EAIAccount." } } 403 { $result.ErrorType = 'AuthorizationError' $result.ErrorId = 'EAI.AuthorizationError' $result.Category = [System.Management.Automation.ErrorCategory]::PermissionDenied } 422 { $result.ErrorType = 'PayloadValidationError' $result.ErrorId = 'EAI.PayloadValidationError' $result.Category = [System.Management.Automation.ErrorCategory]::InvalidData } { $_ -ge 500 } { $result.ErrorType = 'ServerError' $result.ErrorId = 'EAI.ServerError' $result.Category = [System.Management.Automation.ErrorCategory]::ResourceUnavailable $result.ShouldRetry = $true } } return $result } function Get-EAIHttpErrorDetails { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) $statusCode = $null $responseBody = $null if ($ErrorRecord.ErrorDetails -and $ErrorRecord.ErrorDetails.Message) { $responseBody = $ErrorRecord.ErrorDetails.Message } $response = $ErrorRecord.Exception.Response if ($response) { if ($response -is [System.Net.Http.HttpResponseMessage]) { $statusCode = [int]$response.StatusCode if (-not $responseBody -and $response.Content) { $responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() } } elseif ($response -is [System.Net.HttpWebResponse]) { $statusCode = [int]$response.StatusCode if (-not $responseBody) { $reader = [System.IO.StreamReader]::new($response.GetResponseStream()) try { $responseBody = $reader.ReadToEnd() } finally { $reader.Dispose() } } } else { try { $statusCode = [int]$response.StatusCode } catch { $statusCode = $null } } } if (-not $responseBody) { $responseBody = $ErrorRecord.Exception.Message } return [PSCustomObject]@{ StatusCode = $statusCode Body = $responseBody } } function New-EAIErrorRecord { [CmdletBinding()] param( [Parameter(Mandatory)] $ResolvedError, [Parameter(Mandatory)] [String]$Uri ) return [System.Management.Automation.ErrorRecord]::new( [EAIException]::new($ResolvedError.Message), $ResolvedError.ErrorId, $ResolvedError.Category, $Uri ) } function Invoke-EAIRestMethod { [CmdletBinding()] param( [Parameter(Mandatory)] [String]$Uri, [Parameter(Mandatory)] [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')] [String]$Method, [Parameter(Mandatory)] [Hashtable]$Headers, [Parameter(Mandatory)] [PSCustomObject]$Auth, [String]$Body, [ValidateRange(1, 10)] [Int]$MaxRetries = 3, [Switch]$EnableDebugLogging, [System.Management.Automation.PSCmdlet]$CallerPSCmdlet, [PSCustomObject]$ErrorContext ) $headers = @{} + $Headers if ($headers.ContainsKey('__EAIMethod')) { $headers.Remove('__EAIMethod') | Out-Null } $retryBackoff = 5 $attempt = 0 $lastResolvedError = $null $refreshedAfter401 = $false while ($attempt -lt $MaxRetries) { $attempt++ try { $params = @{ Uri = $Uri Method = $Method Headers = $headers TimeoutSec = 30 ErrorAction = 'Stop' } if ($Body) { $params.Body = $Body } if ($EnableDebugLogging) { Write-Debug "Attempt ${attempt}: $Method $Uri" if ($Body -and $Body.Length -lt 1000) { Write-Debug "Request Body: $Body" } } $response = Invoke-RestMethod @params if ($EnableDebugLogging) { Write-Debug "Request successful on attempt $attempt" } return $response } catch { if ($_.Exception -is [EAIException] -or $_.FullyQualifiedErrorId -like 'EAI.*') { throw $_ } $errorDetails = Get-EAIHttpErrorDetails -ErrorRecord $_ $resolvedError = Resolve-EAIException -StatusCode $errorDetails.StatusCode -ResponseBody $errorDetails.Body -ErrorContext $ErrorContext $lastResolvedError = $resolvedError if ($errorDetails.StatusCode -eq 401 -and -not $refreshedAfter401) { $refreshedAfter401 = $true $null = Get-EAIBearerToken -Auth $Auth -ForceRefresh -CallerPSCmdlet $CallerPSCmdlet $refreshedHeaders = New-EAIHeader -Auth $Auth -Method $Method $headers = @{} + $refreshedHeaders if ($headers.ContainsKey('__EAIMethod')) { $headers.Remove('__EAIMethod') | Out-Null } $attempt-- continue } if ($resolvedError.ShouldRetry -and $attempt -lt $MaxRetries) { if ($EnableDebugLogging -or $VerbosePreference -ne 'SilentlyContinue') { Write-Verbose "Edwin API request failed ($($resolvedError.ErrorType)), retrying (attempt $attempt of $MaxRetries)" } Start-Sleep -Seconds ($retryBackoff * $attempt) continue } if ($errorDetails.StatusCode -ge 400 -and $errorDetails.StatusCode -lt 500) { $errorRecord = New-EAIErrorRecord -ResolvedError $resolvedError -Uri $Uri if ($CallerPSCmdlet) { $CallerPSCmdlet.ThrowTerminatingError($errorRecord) } throw $errorRecord } if ($attempt -ge $MaxRetries) { break } if ($EnableDebugLogging -or $VerbosePreference -ne 'SilentlyContinue') { Write-Verbose "Edwin API request failed ($($resolvedError.ErrorType)), retrying (attempt $attempt of $MaxRetries)" } Start-Sleep -Seconds ($retryBackoff * $attempt) } } $finalResolvedError = if ($lastResolvedError) { $lastResolvedError } else { Resolve-EAIException -StatusCode $null -ResponseBody 'Maximum Edwin API retry attempts exhausted.' } $finalError = New-EAIErrorRecord -ResolvedError $finalResolvedError -Uri $Uri if ($CallerPSCmdlet) { $CallerPSCmdlet.ThrowTerminatingError($finalError) } throw $finalError } |