Private/Invoke-PbiApiRequest.ps1

Function Invoke-PbiApiRequest {
    <#
    .SYNOPSIS
        Sends an HTTP request to a Fabric API endpoint and retrieves the response.
        Takes care of: authentication, 429 throttling, Long-Running-Operation (LRO) response
    #>

    [CmdletBinding()]        
    param(                                    
        [Parameter(Mandatory = $false)] [System.Security.SecureString] $authToken,
        [Parameter(Mandatory = $true)] [string] $uri,
        [Parameter(Mandatory = $false)] [ValidateSet('Get', 'Post', 'Delete', 'Put', 'Patch','GET','POST','DELETE','PUT','PATH')] [string] $method = "Get",
        [Parameter(Mandatory = $false)] $body,        
        [Parameter(Mandatory = $false)] [string] $contentType = "application/json; charset=utf-8",
        [Parameter(Mandatory = $false)] [int] $timeoutSec = 240,       
        [Parameter(Mandatory = $false)] [int] $retryCount = 0
    )
    #$script:fabricToken = $null
    if ($null -eq $authToken) {
        $authToken = Get-FabricApiAuthToken
    }

    try {
        
        $requestUrl = "$($PbiApiUrl)/$uri"

        Write-Log "Calling $requestUrl"
        
        # If need to use -OutFile beware of the following breaking change: https://github.com/PowerShell/PowerShell/issues/20744

        # TODO: use -SkipHttpErrorCheck to read the entire error response, need to find a solution to handle 429 errors: https://stackoverflow.com/questions/75629606/powershell-webrequest-handle-response-code-and-exit

        $ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($authToken)
        try {
            $plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
            $response = Invoke-WebRequest -Headers @{ 'Content-Type'  = $contentType; 'Authorization' = "Bearer {0}" -f $plaintext} -Method $method -Uri $requestUrl -Body $body  -TimeoutSec $timeoutSec     
        # Perform operations with the contents of $plaintext in this section.
        } finally {
        # The following line ensures that sensitive data is not left in memory.
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
        if ($plaintext) {
            $plaintext = $null
        }
        }

        $requestId = [string]$response.Headers.requestId

        Write-Log "RAID: $requestId"

        $lroFailOrNoResultFlag = $false
 
        if ($response.StatusCode -eq 202) {
            do {                
                $asyncUrl = [string]$response.Headers.Location

                Write-Log "LRO - Waiting for request to complete in service."

                Start-Sleep -Seconds 5
                
                $ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($authToken)
                try {
                    $plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
            
                    $response = Invoke-WebRequest -Headers @{ 'Content-Type'  = $contentType; 'Authorization' = "Bearer {0}" -f $plaintext} -Method Get -Uri $asyncUrl
                # Perform operations with the contents of $plaintext in this section.
                } finally {
                # The following line ensures that sensitive data is not left in memory.
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
                if ($plaintext) {
                    $plaintext = $null
                }
                }

                $lroStatusContent = $response.Content | ConvertFrom-Json

            }
            while ($lroStatusContent.status -ine "succeeded" -and $lroStatusContent.status -ine "failed")

            if ($lroStatusContent.status -ieq "succeeded") {
                # Only calls /result if there is a location header, otherwise 'OperationHasNoResult' error is thrown

                $resultUrl = [string]$response.Headers.Location

                if ($resultUrl) {
                    $ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($authToken)
                    try {
                        $plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
                        $response = Invoke-WebRequest -Headers @{ 'Content-Type'  = $contentType; 'Authorization' = "Bearer {0}" -f $plaintext} -Method Get -Uri $resultUrl

                    # Perform operations with the contents of $plaintext in this section.
                    } finally {
                    # The following line ensures that sensitive data is not left in memory.
                    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
                    if ($plaintext) {
                        $plaintext = $null
                    }
                    }
                        
                }
                else {
                    $lroFailOrNoResultFlag = $true
                }
            }
            else {
                $lroFailOrNoResultFlag = $true
                
                if ($lroStatusContent.error) {
                    throw "LRO API Error: '$($lroStatusContent.error.errorCode)' - $($lroStatusContent.error.message)"
                }
            }            
        }

        Write-Log "Request completed."

        #if ($response.StatusCode -in @(200,201) -and $response.Content)
        if (!$lroFailOrNoResultFlag -and $response.Content) {      
                        
            $contentBytes = $response.RawContentStream.ToArray()

            # Test for BOM

            if ($contentBytes[0] -eq 0xef -and $contentBytes[1] -eq 0xbb -and $contentBytes[2] -eq 0xbf) {
                $contentText = [System.Text.Encoding]::UTF8.GetString($contentBytes[0..$contentBytes.Length])                
            }
            else {
                $contentText = $response.Content
            }

            $jsonResult = $contentText | ConvertFrom-Json

            if ($jsonResult.value) {
                $jsonResult = $jsonResult.value
            }

            Write-Output $jsonResult -NoEnumerate
        }        
    }
    catch {
          
        $ex = $_.Exception

        $message = $null

        if ($ex.Response -ne $null) {

            $responseStatusCode = [int]$ex.Response.StatusCode

            if ($responseStatusCode -in @(429)) {
                if ($ex.Response.Headers.RetryAfter) {
                    $retryAfterSeconds = $ex.Response.Headers.RetryAfter.Delta.TotalSeconds + 5
                }

                if (!$retryAfterSeconds) {
                    $retryAfterSeconds = 60
                }

                Write-Log "Exceeded the amount of calls (TooManyRequests - 429), sleeping for $retryAfterSeconds seconds."

                Start-Sleep -Seconds $retryAfterSeconds

                $maxRetries = 3
                
                if ($retryCount -le $maxRetries) {
                    
                    Invoke-FabricApiRequest -authToken $authToken -uri $uri -method $method -body $body -contentType $contentType -timeoutSec $timeoutSec -retryCount ($retryCount + 1)
                }
                else {
                    throw "Exceeded the amount of retries ($maxRetries) after 429 error."
                }
            }
            else {                
                $apiErrorObj = $ex.Response.Headers | ? { $_.key -ieq "x-ms-public-api-error-code" } | Select -First 1

                if ($apiErrorObj) {
                    $apiError = $apiErrorObj.Value[0]
                    
                    if ($apiError -ieq "ItemHasProtectedLabel") {
                        Write-Warning "Item has a protected label."
                    }
                    else {
                        $message = "$($ex.Message); API error code: '$apiError'"

                        throw $message
                    }
                }                
            }
        }
        else {
            $message = "$($ex.Message)"
        }
                
        if ($message) {
            throw $message
        }
            
    }

}