Public/Web/Invoke-CustomWebRequest.ps1
function Invoke-CustomWebRequest { <# .SYNOPSIS Helper function for calling a web service. .DESCRIPTION This function is used by the module to call web services. It is not intended to be called directly. .PARAMETER Method The HTTP method to use. If not specified, it is decided according to whether the Body parameter is given. If not, GET is used, otherwise POST. .PARAMETER Uri Web service call address including query parameters. .PARAMETER Body Object that should be POSTed as request. .PARAMETER ContentType The Content-Type header for the POST method. Default is 'application/json'. .PARAMETER ApiCredential Credential to use for authentication. If not specified, $global:AzureDevOpsApi_ApiCredential (set by Set-AzureDevopsVariables) is used. .PARAMETER RetryCount The maximum number of retry attempts for transient failures. Default is 3. .PARAMETER RetryDelay The base delay in seconds between retry attempts. Default is 1 second. .PARAMETER DisableRetry Disables retry logic completely. #> [CmdletBinding()] param( [AllowNull()] [PSTypeName('PSTypeNames.AzureDevOpsApi.ApiCredential')] [PSCustomObject] $ApiCredential, $Uri, $Body, $Method = 'GET', $ContentType = 'application/json', [hashtable] $Headers, [ValidateRange(0, 10)] [int] $RetryCount, [ValidateRange(0.1, 300)] [double] $RetryDelay, [switch] $DisableRetry ) begin { # Use global configuration as defaults if not specified if (-not $PSBoundParameters.ContainsKey('RetryCount')) { $RetryCount = $global:AzureDevOpsApi_RetryConfig.RetryCount } if (-not $PSBoundParameters.ContainsKey('RetryDelay')) { $RetryDelay = $global:AzureDevOpsApi_RetryConfig.RetryDelay } if (-not $PSBoundParameters.ContainsKey('DisableRetry')) { $DisableRetry = $global:AzureDevOpsApi_RetryConfig.DisableRetry } } process { $params = @{} # If called with headers, use them; otherwise, create new hashtable if (!$Headers) { $Headers = @{} } # Set uri $params['Uri'] = $Uri # Request json response $Headers['Accept'] = 'application/json' # Set body, alter method and content type if body is given if ($Body) { $params['Body'] = $Body if (!$Method -or $Method -eq 'GET') { $Method = 'POST' } if (!$ContentType) { $ContentType = 'application/json' } $params['ContentType'] = $ContentType } # Set requested method if ($Method) { $params['Method'] = $Method } # Determine authorization method, if not given if ($Headers.ContainsKey('Authorization')) { Write-Verbose "Using from Authorization Header" if ($ApiCredential) { Write-Warning "ApiCredential is ignored when Authorization is given in Headers" } } elseif ($ApiCredential) { # Add credentials, if given switch ($ApiCredential.Authorization) { 'Basic' { Write-Verbose "Using Basic authorization" $params['Credential'] = $ApiCredential.Credential } 'OAuth' { Write-Verbose "Using OAuth Token" } 'Bearer' { Write-Verbose "Using Bearer Token" } 'PAT' { Write-Verbose "Using Personal Access Token" } default { Write-Verbose "Using Default credentials" $params['UseDefaultCredentials'] = $true } } # If using tokens, add to headers; # PS 5.1 and 6.0 do not support the Token parameter if ($ApiCredential.Authorization -in 'PAT', 'Bearer', 'OAuth') { # @formatter:off $token = switch (Get-PSVersion) { { $_ -ge '7' } { ConvertFrom-SecureString -AsPlainText -SecureString $ApiCredential.Token } '6' { [System.Net.NetworkCredential]::new('', $ApiCredential.Token).Password } default { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ApiCredential.Token) ) } } # @formatter:on if ($ApiCredential.Authorization -in 'Bearer', 'OAuth') { # Bearer and OAuth tokens are already encoded $encodedToken = $token $Headers['Authorization'] = "Bearer $($encodedToken)" } else { # PAT tokens need to be encoded for Http Basic authentication $encodedToken = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":$($token)")) $Headers['Authorization'] = "Basic $($encodedToken)" } } } else { $params['UseDefaultCredentials'] = $true if ((Get-OSVersion).Platform -notlike 'Win*') { throw "No credentials given, and default credentials are not supported on this platform." } if ($VerbosePreference -eq 'Continue') { Write-Warning "No credentials given, using default credentials." } } # Determine whether to use Invoke-WebRequest or curl do { # Default to Invoke-WebRequest $useInvokeWebRequest = $true # Use Invoke-WebRequest only if # 1. curl is not explicitly requested (TODO: make this configurable) if ($CurlIsRequested -eq $true) { $useInvokeWebRequest = $false break } # 2. Calling curl on PS5 is broken if ((Get-PSVersion) -lt 6) { break } # 3. and we are using default credentials # (curl does not support default credentials) if ($true -eq $params['UseDefaultCredentials']) { break } $useInvokeWebRequest = $false } while ($false) # Add special parameters for Invoke-WebRequest if needed if ($useInvokeWebRequest) { if ((Get-PSVersion) -gt 5) { $params['AllowUnencryptedAuthentication'] = $true $params['SkipCertificateCheck'] = $true } # Prevent errors for users on linux, or without loaded profiles on windows # (Internet Explorer engine is used by default in PS 5.1 and older); # UseBasicParsing is default in PS 6.0 and later if ((Get-PSVersion) -lt 6) { $params['UseBasicParsing'] = $true } } # Write verbose output, if requested. # Invoke-CurlWebRequest has verbose output built in. if ($useInvokeWebRequest) { if ($VerbosePreference -eq 'Continue') { $headersWithValues = ($Headers.GetEnumerator() ` | ForEach-Object { " $($_.Key): $($_.Value)" } ` ) -join "`n" $paramsWithValues = ($params.GetEnumerator() ` | Where-Object { $_.Key -ne 'Headers' } ` | ForEach-Object { " $($_.Key): $($_.Value)" } ` ) -join "`n" Write-Verbose "Invoke-WebRequest `n$($headersWithValues)`n$($paramsWithValues)" } } # Add headers, if not empty if ($Headers) { $params['Headers'] = $Headers } # Execute the web request with retry logic (if enabled) try { $scriptblock = { if ($useInvokeWebRequest) { # Always disable progress bars $ProgressPreference = 'SilentlyContinue' Invoke-WebRequest @params } else { Invoke-CurlWebRequest $params } } if ($DisableRetry.IsPresent -and $DisableRetry -eq $true) { & $scriptblock } else { # Use retry logic $response = Invoke-WithRetry ` -ScriptBlock $scriptblock ` -RetryCount $RetryCount ` -RetryDelay $RetryDelay ` -MaxRetryDelay $global:AzureDevOpsApi_RetryConfig.MaxRetryDelay ` -UseExponentialBackoff $global:AzureDevOpsApi_RetryConfig.UseExponentialBackoff ` -UseJitter $global:AzureDevOpsApi_RetryConfig.UseJitter } } catch { # Use Assert-HttpResponse to parse JSON error messages and provide better error details Assert-HttpResponse -ErrorRecord $_ } # Return the response return $response } } |