Private/Http.ps1

function Invoke-DattoApiHttpRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('GET', 'PUT')]
        [string]$Method,

        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter()]
        [AllowNull()]
        [System.Collections.IDictionary]$Query,

        [Parameter()]
        [AllowNull()]
        [object]$Body,

        [Parameter()]
        [AllowNull()]
        [object]$Connection,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Accept = 'application/json',

        [Parameter()]
        [switch]$AsByteArray,

        [Parameter()]
        [switch]$RetryNonIdempotent
    )

    $connectionState = Resolve-DattoApiConnectionState -Connection $Connection
    $requestUri = New-DattoApiRequestUri -ConnectionState $connectionState -Path $Path -Query $Query
    $isRetrySafe = ($Method -eq 'GET') -or $RetryNonIdempotent
    $maximumRetries = [int]$connectionState.MaxRetryCount
    $transientStatusCodes = @(408, 429, 500, 502, 503, 504)

    for ($attempt = 0; $attempt -le $maximumRetries; $attempt++) {
        $request = $null
        $response = $null
        $authorizationParameter = $null
        try {
            $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $requestUri)
            $authorizationParameter = Get-DattoApiAuthorizationParameter -Credential $connectionState.Credential
            $request.Headers.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Basic', $authorizationParameter)
            $request.Headers.Accept.Clear()
            $request.Headers.Accept.Add([System.Net.Http.Headers.MediaTypeWithQualityHeaderValue]::new($Accept))

            if ($null -ne $Body) {
                $jsonBody = $Body | ConvertTo-Json -Depth 100 -Compress
                $request.Content = [System.Net.Http.StringContent]::new($jsonBody, [Text.Encoding]::UTF8, 'application/json')
            }

            Write-Verbose ("Datto API {0} {1}; attempt {2} of {3}." -f $Method, $requestUri.AbsolutePath, ($attempt + 1), ($maximumRetries + 1))
            try {
                $response = $connectionState.Client.SendAsync($request).GetAwaiter().GetResult()
            }
            catch {
                if ($isRetrySafe -and $attempt -lt $maximumRetries) {
                    $delay = Get-DattoApiRetryDelayMilliseconds -Attempt ($attempt + 1) -Headers @{} -MaximumDelaySeconds $connectionState.MaxRetryDelaySeconds
                    Write-Verbose ("Transient transport failure. Retrying after {0} ms." -f $delay)
                    Start-Sleep -Milliseconds $delay
                    continue
                }
                $transportError = New-DattoApiTransportErrorRecord -Exception $_.Exception -Method $Method -RequestUri $requestUri
                $PSCmdlet.ThrowTerminatingError($transportError)
            }

            $headers = ConvertFrom-DattoApiHeaders -Response $response
            Update-DattoApiRateLimit -ConnectionState $connectionState -Headers $headers
            $connectionState.LastUsedAtUtc = [datetimeoffset]::UtcNow
            $statusCode = [int]$response.StatusCode
            $reasonPhrase = [string]$response.ReasonPhrase

            if (-not $response.IsSuccessStatusCode) {
                $responseText = $null
                if ($null -ne $response.Content) {
                    $responseText = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
                }

                if ($isRetrySafe -and $attempt -lt $maximumRetries -and $transientStatusCodes -contains $statusCode) {
                    $delay = Get-DattoApiRetryDelayMilliseconds -Attempt ($attempt + 1) -Headers $headers -MaximumDelaySeconds $connectionState.MaxRetryDelaySeconds -UseRateLimitReset:($statusCode -eq 429)
                    Write-Verbose ("Datto API returned HTTP {0}. Retrying after {1} ms." -f $statusCode, $delay)
                    Start-Sleep -Milliseconds $delay
                    continue
                }
                $httpError = New-DattoApiErrorRecord -StatusCode $statusCode -ReasonPhrase $reasonPhrase -Method $Method -RequestUri $requestUri -ResponseText $responseText
                $PSCmdlet.ThrowTerminatingError($httpError)
            }

            $contentType = $null
            if ($null -ne $response.Content -and $null -ne $response.Content.Headers.ContentType) {
                $contentType = [string]$response.Content.Headers.ContentType.MediaType
            }

            $bytes = $null
            $parsedBody = $null
            if ($null -ne $response.Content) {
                if ($AsByteArray) {
                    $bytes = $response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult()
                }
                else {
                    $text = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
                    if (-not [string]::IsNullOrWhiteSpace($text)) {
                        if ([string]::IsNullOrWhiteSpace($contentType) -or $contentType -match 'json') {
                            try {
                                $convertedBody = $text | ConvertFrom-Json -ErrorAction Stop
                                if ($text.TrimStart().StartsWith('[')) {
                                    $parsedBody = [object[]]@($convertedBody)
                                }
                                else {
                                    $parsedBody = $convertedBody
                                }
                            }
                            catch {
                                throw [System.IO.InvalidDataException]::new("Datto returned a successful response that could not be parsed as JSON for $Method $($requestUri.AbsolutePath).", $_.Exception)
                            }
                        }
                        else {
                            $parsedBody = $text
                        }
                    }
                }
            }

            $result = [pscustomobject][ordered]@{
                StatusCode  = $statusCode
                ReasonPhrase = $reasonPhrase
                RequestUri  = $requestUri
                Headers     = $headers
                ContentType = $contentType
                Body        = $parsedBody
                Bytes       = $bytes
                ReceivedAtUtc = [datetimeoffset]::UtcNow
            }
            $result.PSObject.TypeNames.Insert(0, 'DattoBackupApi.Response')
            return $result
        }
        finally {
            $authorizationParameter = $null
            if ($null -ne $response) { $response.Dispose() }
            if ($null -ne $request) { $request.Dispose() }
        }
    }

    throw [System.InvalidOperationException]::new('The Datto API retry loop ended unexpectedly.')
}