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.') } |