Private/Invoke-ConfluenceHttpRequest.ps1
|
function Invoke-ConfluenceHttpRequest { <# .SYNOPSIS Sends one HTTP request to Confluence and returns the status code, body and Retry-After value. .DESCRIPTION Wraps Invoke-WebRequest so HTTP error responses are returned instead of thrown on both Windows PowerShell 5.1 and PowerShell 7. Returns an object with StatusCode, StatusDescription, Content and RetryAfter. Transport failures (DNS, TLS, timeouts) are rethrown. The request headers, which carry the credential, are never written to any output stream. A 429 Too Many Requests response is retried up to -MaxRetry times. The wait is the Retry-After header (seconds or an HTTP date, capped at 60 seconds) or, without that header, 2, 4, 8 ... seconds. #> [CmdletBinding()] [OutputType([pscustomobject])] param ( [Parameter(Mandatory)] [string]$Uri, [Parameter(Mandatory)] [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method, [string]$Body, [byte[]]$BodyBytes, [string]$ContentType = 'application/json; charset=utf-8', [hashtable]$AdditionalHeaders, [ValidateRange(0, 10)] [int]$MaxRetry = 4 ) $attempt = 0 while ($true) { $response = Invoke-ConfluenceHttpRequestOnce -Uri $Uri -Method $Method -Body $Body -BodyBytes $BodyBytes -ContentType $ContentType -AdditionalHeaders $AdditionalHeaders if ($response.StatusCode -ne 429 -or $attempt -ge $MaxRetry) { return $response } $attempt++ $delaySeconds = [math]::Pow(2, $attempt) if ($response.RetryAfter) { $seconds = 0 $date = [datetime]::MinValue if ([int]::TryParse("$($response.RetryAfter)", [ref]$seconds)) { $delaySeconds = $seconds } elseif ([datetime]::TryParse("$($response.RetryAfter)", [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref]$date)) { $delaySeconds = [math]::Ceiling(($date - [datetime]::UtcNow).TotalSeconds) } } $delaySeconds = [math]::Min([math]::Max($delaySeconds, 0), 60) Write-Verbose "Confluence returned 429 Too Many Requests; retry $attempt of $MaxRetry in $delaySeconds second(s)." Start-Sleep -Milliseconds ([int]($delaySeconds * 1000)) } } function Invoke-ConfluenceHttpRequestOnce { <# .SYNOPSIS Sends a single HTTP request without retrying. Used by Invoke-ConfluenceHttpRequest. #> [CmdletBinding()] [OutputType([pscustomobject])] param ( [Parameter(Mandatory)] [string]$Uri, [Parameter(Mandatory)] [string]$Method, [string]$Body, [byte[]]$BodyBytes, [string]$ContentType, [hashtable]$AdditionalHeaders ) $headers = Get-ConfluenceAuthHeader if ($AdditionalHeaders) { foreach ($key in @($AdditionalHeaders.get_Keys())) { $headers[$key] = $AdditionalHeaders[$key] } } $requestParams = @{ Uri = $Uri Method = $Method Headers = $headers ContentType = $ContentType UseBasicParsing = $true ErrorAction = 'Stop' } if ($null -ne $BodyBytes -and $BodyBytes.Length -gt 0) { $requestParams.Body = $BodyBytes } elseif (-not [string]::IsNullOrEmpty($Body)) { # Send UTF-8 bytes so non-ASCII page content survives on Windows PowerShell 5.1 $requestParams.Body = [System.Text.Encoding]::UTF8.GetBytes($Body) } if ($PSVersionTable.PSVersion.Major -ge 7) { $requestParams.SkipHttpErrorCheck = $true } try { $response = Invoke-WebRequest @requestParams return [pscustomobject]@{ StatusCode = [int]$response.StatusCode StatusDescription = [string]$response.StatusDescription Content = [string]$response.Content RetryAfter = (Get-ConfluenceRetryAfterValue -Headers $response.Headers) } } catch { $requestError = $_ $webResponse = $null if ($requestError.Exception.PSObject.Properties.Name -contains 'Response') { $webResponse = $requestError.Exception.Response } if ($null -eq $webResponse) { throw } # Windows PowerShell 5.1 throws on HTTP errors; recover the status and body from the exception $content = '' if ($requestError.ErrorDetails -and $requestError.ErrorDetails.Message) { $content = $requestError.ErrorDetails.Message } else { try { $reader = New-Object -TypeName System.IO.StreamReader -ArgumentList $webResponse.GetResponseStream() $content = $reader.ReadToEnd() $reader.Dispose() } catch { Write-Verbose "Could not read the error response body: $($_.Exception.Message)" } } return [pscustomobject]@{ StatusCode = [int]$webResponse.StatusCode StatusDescription = [string]$webResponse.StatusDescription Content = [string]$content RetryAfter = (Get-ConfluenceRetryAfterValue -Headers $webResponse.Headers) } } } function Get-ConfluenceRetryAfterValue { <# .SYNOPSIS Returns the Retry-After header value from a response header collection, or nothing. #> [CmdletBinding()] [OutputType([string])] param ( $Headers ) if ($null -eq $Headers) { return } # Dictionaries (PowerShell 7, and Windows PowerShell for successful responses) throw for a # missing key; WebHeaderCollection returns $null if ($Headers -is [System.Collections.IDictionary] -and -not ([System.Collections.IDictionary]$Headers).Contains('Retry-After')) { return } $value = $null try { $value = $Headers['Retry-After'] } catch { Write-Verbose 'The response headers could not be read.' } if ($null -eq $value) { return } # PowerShell 7 returns a string[] per header; Windows PowerShell returns a string return [string](@($value)[0]) } |