Public/Invoke-ControlAPIMaster.ps1

function Invoke-ControlAPIMaster {
    <#
    .SYNOPSIS
        Internal function used to make API calls
    .DESCRIPTION
        Internal function used to make API calls
    .PARAMETER Arguments
        Required parameters for the API call
        A URI without a leading "/" will default to the Automate Extension path.
        A URI without a protocol/server will default to the Control Server established by Connect-ControlAPI
    .OUTPUTS
        The returned results from the API call
    .NOTES
        Version: 1.0
        Author: Darren White
        Creation Date: 2020-08-01
        Purpose/Change: Initial script development

        Version: 1.1.0
        Author: Darren White
        Creation Date: 2020-12-01
        Purpose/Change: Include values in $Script:CWCHeaders variable in request

    .EXAMPLE
        $APIRequest = @{
            'URI' = "ReportService.ashx/GenerateReportForAutomate"
            'Body' = ConvertTo-Json @("Session","",@('SessionID','SessionType','Name','CreatedTime'),"NOT IsEnded", "", 10000)
        }
        $AllSessions = Invoke-ControlAPIMaster -Arguments $APIRequest
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        $Arguments,
        [int]$MaxRetry = 3
    )

    Begin { 
        $Result = $Null
    }

    Process {
        # Check that we have cached connection info
        If(!$Script:CWCIsConnected) {
            $ErrorMessage = @()
            $ErrorMessage += "Not connected to a Control server."
            $ErrorMessage +=  $_.ScriptStackTrace
            $ErrorMessage += ''
            $ErrorMessage += "----> Run 'Connect-ControlAPI' to initialize the connection before issuing other ControlAPI commands."
            Write-Error ($ErrorMessage | Out-String)
            Return
        }

        # Add default set of arguments
        $Arguments.Item('UseBasicParsing')=$Null
        If (!$Arguments.Headers) {$Arguments.Headers=@{}}
        Foreach($Key in $script:CWCHeaders.Keys){
            If($Arguments.Headers.Keys -notcontains $Key){
                $Arguments.Headers += @{$Key = $script:CWCHeaders.$Key}
            }
        }
        If ($Script:ControlAPIKey) {
            $Arguments.Headers.Item('CWAIKToken') = (Get-CWAIKToken)
        } ElseIf (!$Arguments.Headers.Authorization) {
            $Authstring  = "$($Script:ControlAPICredentials.UserName):$($Script:ControlAPICredentials.GetNetworkCredential().Password)"
            $encodedAuth  = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($Authstring));
            $Arguments.Headers.Item('Authorization') = "Basic $encodedAuth"
        }

        # Check URI format
        if($Arguments.URI -notlike '*`?*' -and $Arguments.URI -like '*`&*') {
            $Arguments.URI = $Arguments.URI -replace '(.*?)&(.*)', '$1?$2'
        }        
        if($Arguments.URI -notmatch '^(https?://|/)') {
            $Arguments.URI = "/App_Extensions/${Script:CWCExtensionID}/$($Arguments.URI)"
        }
        if($Arguments.URI -notmatch '^https?://') {
            $Arguments.URI = "${Script:ControlServer}$($Arguments.URI)"
        }

        If(!$Arguments.ContainsKey('Method')) {
            $Arguments.Add('Method','POST')
        }
        If(!$Arguments.ContainsKey('ContentType')) {
            $Arguments.Add('ContentType','application/json; charset=utf-8')
        }

        # Issue request
        Write-Debug "Calling Control Server Extension with the following arguments:`n$(($Arguments|Out-String -Stream) -join "`n")"
        Try {
            $ProgressPreference = 'SilentlyContinue'
            $Result = Invoke-WebRequest @Arguments
        } Catch {
            # Start error message
            $ErrorMessage = @()
            If($_.Exception.Response){
                # Read exception response
                $ErrorStream = $_.Exception.Response.GetResponseStream()
                $Reader = New-Object System.IO.StreamReader($ErrorStream)
                $global:ErrBody = $Reader.ReadToEnd() | ConvertFrom-Json
                $Result=$_.Exception | Select-Object -ExpandProperty Response

                If($errBody.code){
                    $ErrorMessage += "An exception has been thrown."
                    $ErrorMessage +=  $_.ScriptStackTrace
                    $ErrorMessage += ''    
                    $ErrorMessage += "--> $($ErrBody.code)"
                    If($errBody.code -eq 'Unauthorized'){
                        $Script:CWCIsConnected=$False
                        $ErrorMessage += "-----> $($ErrBody.message)"
                        $ErrorMessage += "-----> Use 'Connect-ControlAPI' to set new authentication."
                    } Else {
                        $ErrorMessage += "-----> $($ErrBody.message)"
                        $ErrorMessage += "-----> ^ Error has not been documented please report. ^"
                    }
                }
            }

            If ($_.ErrorDetails) {
                $Result=$Result | Select-Object -ExcludeProperty Content -Property *,@{n='Content';e={$_.Exception.Message}}
                $ErrorMessage += "An error has been thrown."
                $ErrorMessage +=  $_.ScriptStackTrace
                $ErrorMessage += ''
                $global:errDetails = $_.ErrorDetails
                $ErrorMessage += "--> $($errDetails.code)"
                $ErrorMessage += "--> $($errDetails.message)"
                If($errDetails.errors.message){
                    $ErrorMessage += "-----> $($errDetails.errors.message)"
                }
                If($errDetails.message -match 'Unauthorized'){
                    $Script:CWCIsConnected=$False
                    $ErrorMessage += "-----> Use 'Connect-ControlAPI' to set new authentication."
                }
            }
            If (!$ErrorMessage) {$ErrorMessage+='An unknown error was returned'; $ErrorMessage+=$Result|Out-String -Stream}
            Write-Error ($ErrorMessage | Out-String)
            If ($Result.StatusCode -ne 500 ) {Return}
        }

        # Not sure this will be hit with current iwr error handling
        # May need to move to catch block need to find test
        # TODO Find test for retry
        # Retry the request
        $Retry = 0
        while ($Retry -lt $MaxRetry -and $Result.StatusCode -eq 500) {
            $Retry++
            $Wait = $([math]::pow( 2, $Retry))
            Write-Warning "Issue with request, status: $($Result.StatusCode.Value__) $($Result.StatusDescription)"
            Write-Warning "$($Retry)/$($MaxRetry) retries, waiting $($Wait)s."
            Start-Sleep -Seconds $Wait
            $ProgressPreference = 'SilentlyContinue'
            Try {
                $Result = Invoke-WebRequest @Arguments
            } Catch {
                $Result=$_.Exception | Select-Object -ExpandProperty Response
                $Result=$Result | Select-Object -ExcludeProperty Content -Property *,@{n='Content';e={$_.Exception.Message}}
            }
        }
        If ($Retry -ge $MaxRetry -and $Result.StatusCode -eq 500) {
            Write-Error "Max retries hit. Status: $($Result.StatusCode) $($Result.StatusDescription)"
            Return
        }
    }

    End {
        If ($Result) {
            Try {
                Get-Variable -Name CWCServerTime -Scope 1 -ErrorAction Stop
                Set-Variable -Name CWCServerTime -Scope 1 -Value (Get-Date $($Result.Headers.Date))
            } Catch {}
            $SCData=$(Try {$Result.Content | ConvertFrom-Json} Catch {})
            If ($SCData -and @($SCData.PSObject.Properties.Name) -contains 'FieldNames' -and $SCData.Items -and $SCData.Items.Count -gt 0) {
                $FNames = $SCData.FieldNames
                $SCData.Items | ForEach-Object {
                    $x = $_
                    $SCEventRecord = @{}
                    For ($i = 0; $i -lt $FNames.Length; $i++) {
                        $Null = $SCEventRecord.Add($FNames[$i],$x[$i])
                    }
                    [pscustomobject]$SCEventRecord
                }
            } ElseIf ($SCData) {
                $SCData
            } Else {
                $Result.Content
            }
        }
        Return
    }
}