Private/Invoke-AACHttp.ps1
|
function Invoke-AACHttp { <# .SYNOPSIS Calls an Azure API once, through the pooled HttpClient, retrying what is worth retrying - and returns the response's status and text, or throws with Azure's own reason. .DESCRIPTION Every Azure call of the module goes through here (Invoke-AACArmRequest for Resource Manager, Invoke-AACResourceGraphQuery, Invoke-AACCostQuery) or through Invoke-AACArmParallel, with the same rules: - throttling (429) and server errors (5xx) are retried up to four times, waiting as long as the longest retry header asks (Get-AACRetryDelay), else 5, 10, 15, 20 seconds; never more than 60 at a time - a dropped connection is retried after a second or two - any other failure throws, with the HTTP status in the exception's Data['StatusCode'] and Azure's message - with the query APIs' nested innererror detail - as its message (Get-AACErrorMessage) Returns @{ Status; Content (the response text); Headers (the response's headers, content headers included, case-insensitive) }. -Resource picks the token's audience (Get-AACAccessToken -Resource). A failure's exception also carries Data['Code'] (Azure's error code, e.g. RequestDisallowedByPolicy) and Data['Content'] (the body). #> [CmdletBinding()] [OutputType([hashtable])] param( [ValidateSet('Get', 'Post', 'Put', 'Patch', 'Delete', 'Head')] [string] $Method = 'Get', [Parameter(Mandatory)] [string] $Uri, [string] $Body, [byte[]] $BodyBytes, [string] $ContentType = 'application/octet-stream', [System.Collections.IDictionary] $Header, [string] $Resource = 'https://management.azure.com', [ValidateRange(1, 10)] [int] $MaxAttempts = 5 ) for ($attempt = 1; ; $attempt++) { $send = @{ Method = $Method; Uri = $Uri; Token = (Get-AACAccessToken -Resource $Resource) } if ($PSBoundParameters.ContainsKey('Body')) { $send.Body = $Body } if ($PSBoundParameters.ContainsKey('BodyBytes')) { $send.BodyBytes = $BodyBytes; $send.ContentType = $ContentType } if ($Header) { $send.Header = $Header } $response = $null $status = 0 $content = '' $failure = '' try { $response = (Send-AACHttpRequest @send).GetAwaiter().GetResult() $status = [int]$response.StatusCode $content = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() } catch { $failure = $_.Exception.GetBaseException().Message } try { if ($status -ge 200 -and $status -lt 300) { $headers = [System.Collections.Generic.Dictionary[string, string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($pair in @($response.Headers) + @($response.Content.Headers)) { $headers[$pair.Key] = ($pair.Value -join ',') } return @{ Status = $status; Content = $content; Headers = $headers } } if ($attempt -lt $MaxAttempts -and ($status -eq 0 -or $status -eq 429 -or $status -ge 500)) { $wait = Get-AACRetryDelay -Response $response -Status $status -Attempt $attempt Start-Sleep -Milliseconds ([int](1000 * $wait)) continue } # Azure's own reason, with the nested detail the query APIs give. $message = Get-AACErrorMessage -Content $content -Fallback $(if ($failure) { $failure } else { "Response status code does not indicate success: $status ($($response.ReasonPhrase))." }) $exception = [System.Exception]::new($message) $exception.Data['StatusCode'] = $status $exception.Data['Content'] = $content $exception.Data['Code'] = $(try { [string](ConvertFrom-Json -InputObject $content -AsHashtable -ErrorAction Stop)['error']['code'] } catch { '' }) throw $exception } finally { if ($response) { $response.Dispose() } } } } |