Private/Invoke-AzLocalItsmHttp.ps1
|
function Invoke-AzLocalItsmHttp { <# .SYNOPSIS Shared HTTP layer for ITSM connector adapters. .DESCRIPTION Wraps Invoke-RestMethod with: - TLS 1.2+ enforced - 30s default timeout - Honour Retry-After on HTTP 429 / 503 - Exponential backoff capped at 3 retry attempts (1s, 2s, 4s) - Structured Write-Verbose logging with secret redaction Returns the parsed response object. Throws on non-retryable errors and on retry-exhaustion. Designed to be mocked in tests via Mock Invoke-AzLocalItsmHttp. #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory = $true)][ValidateSet('GET','POST','PUT','PATCH','DELETE')][string]$Method, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Uri, [Parameter(Mandatory = $false)][hashtable]$Headers, [Parameter(Mandatory = $false)][object]$Body, [Parameter(Mandatory = $false)][string]$ContentType = 'application/json', [Parameter(Mandatory = $false)][int]$TimeoutSec = 30, [Parameter(Mandatory = $false)][int]$MaxAttempts = 3 ) # Enforce TLS 1.2+ once per session (idempotent). try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { Write-Verbose "Invoke-AzLocalItsmHttp: could not enable TLS 1.2 on ServicePointManager: $($_.Exception.Message)" } $params = @{ Method = $Method Uri = $Uri ContentType = $ContentType TimeoutSec = $TimeoutSec ErrorAction = 'Stop' } if ($Headers) { $params['Headers'] = $Headers } if ($null -ne $Body -and $Method -in 'POST','PUT','PATCH') { if ($Body -is [string] -or $Body -is [byte[]]) { # Pass-through: caller is responsible for wire format (form-urlencoded # string, raw octet-stream bytes for attachments, etc.). $params['Body'] = $Body } else { $params['Body'] = ($Body | ConvertTo-Json -Depth 12 -Compress) } } $attempt = 0 while ($true) { $attempt++ try { $redactedUri = $Uri -replace '(client_secret|access_token|password)=[^&]+', '$1=***' Write-Verbose "Invoke-AzLocalItsmHttp: attempt $attempt $Method $redactedUri" return Invoke-RestMethod @params } catch { $ex = $_.Exception $status = 0 $retryAfter = 0 if ($ex.PSObject.Properties['Response'] -and $ex.Response) { try { $status = [int]$ex.Response.StatusCode $raHeader = $ex.Response.Headers['Retry-After'] if ($raHeader) { [int]::TryParse($raHeader, [ref]$retryAfter) | Out-Null } } catch { Write-Verbose "Invoke-AzLocalItsmHttp: failed to extract status from response: $($_.Exception.Message)" } } $retryable = $status -in 429,500,502,503,504 if (-not $retryable -or $attempt -ge $MaxAttempts) { throw [System.Exception]::new("ITSM HTTP $Method $Uri failed (status=$status, attempt=$attempt): $($ex.Message)", $ex) } if ($retryAfter -le 0) { $retryAfter = [Math]::Pow(2, $attempt - 1) } Write-Verbose "Invoke-AzLocalItsmHttp: status=$status, sleeping ${retryAfter}s before retry (attempt $attempt/$MaxAttempts)." Start-Sleep -Seconds $retryAfter } } } |