Skip to content

Commit 99dfb71

Browse files
Refactor HTTP requests to use centralized Invoke-HttpRequestWithRetry (#529)
## Summary Introduces a new `Invoke-HttpRequestWithRetry` cmdlet that centralizes HTTP retry logic for transient errors (408, 429, 500, 502, 503, 504) and refactors existing callers to use it. ## Changes - **New cmdlet**: `Invoke-HttpRequestWithRetry` in `Private/Shared` — wraps `Invoke-WebRequest` with configurable retry count, interval, and transient status code handling - **Refactored** `Invoke-GitHubApiRequest` to delegate retry logic to the new shared cmdlet instead of implementing its own retry loop - **Updated** `Test-NetworkConnectivity` to use `Invoke-HttpRequestWithRetry` instead of calling `Invoke-WebRequest` directly - **Updated** `Get-TerraformTool` to use `Invoke-HttpRequestWithRetry` for HashiCorp API calls and file downloads - **Updated** unit tests to mock `Invoke-HttpRequestWithRetry` instead of `Invoke-WebRequest` ## Benefits - Single place to maintain retry logic, reducing duplication - Consistent retry behavior across all HTTP calls in the module - Easier to test and extend retry behavior in the future
1 parent fffb47e commit 99dfb71

21 files changed

Lines changed: 542 additions & 115 deletions

src/ALZ/Private/Deploy-Accelerator-Helpers/New-FolderStructure.ps1

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ function New-FolderStructure {
2626
[switch] $replaceFiles,
2727

2828
[Parameter(Mandatory = $false)]
29-
[int] $maxRetryCount = 10
29+
[int] $maxRetryCount = 10,
30+
31+
[Parameter(Mandatory = $false)]
32+
[int] $retryIntervalSeconds = 3,
33+
34+
[Parameter(Mandatory = $false)]
35+
[int] $httpRequestTimeoutSeconds
3036
)
3137

3238
if ($PSCmdlet.ShouldProcess("ALZ-Terraform module configuration", "modify")) {
@@ -54,7 +60,20 @@ function New-FolderStructure {
5460
}
5561

5662
} else {
57-
$releaseTag = Get-GithubRelease -githubRepoUrl $url -targetDirectory $targetDirectory -moduleSourceFolder $sourceFolder -moduleTargetFolder $targetFolder -release $release -releaseArtifactName $releaseArtifactName -maxRetryCount $maxRetryCount
63+
$releaseParams = @{
64+
githubRepoUrl = $url
65+
targetDirectory = $targetDirectory
66+
moduleSourceFolder = $sourceFolder
67+
moduleTargetFolder = $targetFolder
68+
release = $release
69+
releaseArtifactName = $releaseArtifactName
70+
maxRetryCount = $maxRetryCount
71+
retryIntervalSeconds = $retryIntervalSeconds
72+
}
73+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
74+
$releaseParams["httpRequestTimeoutSeconds"] = $httpRequestTimeoutSeconds
75+
}
76+
$releaseTag = Get-GithubRelease @releaseParams
5877
$path = Join-Path $targetDirectory $targetFolder $releaseTag
5978
}
6079

src/ALZ/Private/Deploy-Accelerator-Helpers/New-ModuleSetup.ps1

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ function New-ModuleSetup {
2525
[Parameter(Mandatory = $false)]
2626
[switch]$autoApprove,
2727
[Parameter(Mandatory = $false)]
28-
[int]$maxRetryCount = 10
28+
[int]$maxRetryCount = 10,
29+
[Parameter(Mandatory = $false)]
30+
[int]$retryIntervalSeconds = 3,
31+
[Parameter(Mandatory = $false)]
32+
[int]$httpRequestTimeoutSeconds
2933
)
3034

3135
if ($PSCmdlet.ShouldProcess("Check and get module", "modify")) {
@@ -44,21 +48,36 @@ function New-ModuleSetup {
4448

4549
if(-not [string]::IsNullOrWhiteSpace($moduleOverrideFolderPath)) {
4650
Write-Verbose "Using module override folder path, skipping version checks."
47-
return New-FolderStructure `
48-
-targetDirectory $targetDirectory `
49-
-url $url `
50-
-release $desiredRelease `
51-
-releaseArtifactName $releaseArtifactName `
52-
-targetFolder $targetFolder `
53-
-sourceFolder $sourceFolder `
54-
-overrideSourceDirectoryPath $moduleOverrideFolderPath `
55-
-replaceFiles:$replaceFiles.IsPresent `
56-
-maxRetryCount $maxRetryCount
51+
$folderParams = @{
52+
targetDirectory = $targetDirectory
53+
url = $url
54+
release = $desiredRelease
55+
releaseArtifactName = $releaseArtifactName
56+
targetFolder = $targetFolder
57+
sourceFolder = $sourceFolder
58+
overrideSourceDirectoryPath = $moduleOverrideFolderPath
59+
replaceFiles = $replaceFiles.IsPresent
60+
maxRetryCount = $maxRetryCount
61+
retryIntervalSeconds = $retryIntervalSeconds
62+
}
63+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
64+
$folderParams["httpRequestTimeoutSeconds"] = $httpRequestTimeoutSeconds
65+
}
66+
return New-FolderStructure @folderParams
5767
}
5868

5969
$latestReleaseTag = $null
6070
try {
61-
$latestResult = Get-GithubReleaseTag -githubRepoUrl $url -release "latest" -maxRetryCount $maxRetryCount
71+
$releaseTagParams = @{
72+
githubRepoUrl = $url
73+
release = "latest"
74+
maxRetryCount = $maxRetryCount
75+
retryIntervalSeconds = $retryIntervalSeconds
76+
}
77+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
78+
$releaseTagParams["httpRequestTimeoutSeconds"] = $httpRequestTimeoutSeconds
79+
}
80+
$latestResult = Get-GithubReleaseTag @releaseTagParams
6281
$latestReleaseTag = $latestResult.ReleaseTag
6382
Write-Verbose "Latest available $targetFolder version: $latestReleaseTag"
6483
} catch {
@@ -136,16 +155,22 @@ function New-ModuleSetup {
136155
Write-ToConsoleLog "Downloading $targetFolder module version $desiredRelease" -IsSuccess
137156
}
138157

139-
$versionAndPath = New-FolderStructure `
140-
-targetDirectory $targetDirectory `
141-
-url $url `
142-
-release $desiredRelease `
143-
-releaseArtifactName $releaseArtifactName `
144-
-targetFolder $targetFolder `
145-
-sourceFolder $sourceFolder `
146-
-overrideSourceDirectoryPath $moduleOverrideFolderPath `
147-
-replaceFiles:$replaceFiles.IsPresent `
148-
-maxRetryCount $maxRetryCount
158+
$downloadFolderParams = @{
159+
targetDirectory = $targetDirectory
160+
url = $url
161+
release = $desiredRelease
162+
releaseArtifactName = $releaseArtifactName
163+
targetFolder = $targetFolder
164+
sourceFolder = $sourceFolder
165+
overrideSourceDirectoryPath = $moduleOverrideFolderPath
166+
replaceFiles = $replaceFiles.IsPresent
167+
maxRetryCount = $maxRetryCount
168+
retryIntervalSeconds = $retryIntervalSeconds
169+
}
170+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
171+
$downloadFolderParams["httpRequestTimeoutSeconds"] = $httpRequestTimeoutSeconds
172+
}
173+
$versionAndPath = New-FolderStructure @downloadFolderParams
149174

150175
Write-Verbose "New version: $($versionAndPath.releaseTag) at path: $($versionAndPath.path)"
151176

src/ALZ/Private/Shared/Get-GithubRelease.ps1

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,31 @@ function Get-GithubRelease {
4949

5050
[Parameter(Mandatory = $false, HelpMessage = "Maximum number of retries for transient GitHub API errors.")]
5151
[int]
52-
$maxRetryCount = 10
52+
$maxRetryCount = 10,
53+
54+
[Parameter(Mandatory = $false, HelpMessage = "Seconds to wait between retries for transient HTTP request errors.")]
55+
[int]
56+
$retryIntervalSeconds = 3,
57+
58+
[Parameter(Mandatory = $false, HelpMessage = "Timeout in seconds for HTTP requests.")]
59+
[int]
60+
$httpRequestTimeoutSeconds
5361
)
5462

5563
$parentDirectory = $targetDirectory
5664
$targetPath = Join-Path $targetDirectory $moduleTargetFolder
5765

5866
# Get the release tag and data from GitHub
59-
$releaseResult = Get-GithubReleaseTag -githubRepoUrl $githubRepoUrl -release $release -maxRetryCount $maxRetryCount
67+
$releaseTagParams = @{
68+
githubRepoUrl = $githubRepoUrl
69+
release = $release
70+
maxRetryCount = $maxRetryCount
71+
retryIntervalSeconds = $retryIntervalSeconds
72+
}
73+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
74+
$releaseTagParams["httpRequestTimeoutSeconds"] = $httpRequestTimeoutSeconds
75+
}
76+
$releaseResult = Get-GithubReleaseTag @releaseTagParams
6077
$releaseTag = $releaseResult.ReleaseTag
6178
$releaseData = $releaseResult.ReleaseData
6279

@@ -100,7 +117,16 @@ function Get-GithubRelease {
100117

101118
Write-Verbose "===> Downloading the release artifact $releaseArtifactUrl from the GitHub repository $repoOrgPlusRepo"
102119

103-
Invoke-GitHubApiRequest -Uri $releaseArtifactUrl -OutputFile $targetPathForZip -MaxRetryCount $maxRetryCount -RetryIntervalSeconds 3
120+
$downloadParams = @{
121+
Uri = $releaseArtifactUrl
122+
OutputFile = $targetPathForZip
123+
MaxRetryCount = $maxRetryCount
124+
RetryIntervalSeconds = $retryIntervalSeconds
125+
}
126+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
127+
$downloadParams["TimeoutSec"] = $httpRequestTimeoutSeconds
128+
}
129+
Invoke-GitHubApiRequest @downloadParams
104130

105131
if(!(Test-Path $targetPathForZip)) {
106132
Write-ToConsoleLog "Failed to download the release $releaseTag from the GitHub repository $repoOrgPlusRepo" -IsError

src/ALZ/Private/Shared/Get-GithubReleaseTag.ps1

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ function Get-GithubReleaseTag {
3333

3434
[Parameter(Mandatory = $false, HelpMessage = "Maximum number of retries for transient GitHub API errors.")]
3535
[int]
36-
$maxRetryCount = 10
36+
$maxRetryCount = 10,
37+
38+
[Parameter(Mandatory = $false, HelpMessage = "Seconds to wait between retries for transient HTTP request errors.")]
39+
[int]
40+
$retryIntervalSeconds = 3,
41+
42+
[Parameter(Mandatory = $false, HelpMessage = "Timeout in seconds for HTTP requests.")]
43+
[int]
44+
$httpRequestTimeoutSeconds
3745
)
3846

3947
# Split Repo URL into parts
@@ -48,7 +56,16 @@ function Get-GithubReleaseTag {
4856
}
4957

5058
# Query the GitHub API
51-
$response = Invoke-GitHubApiRequest -Uri $repoReleaseUrl -SkipHttpErrorCheck -MaxRetryCount $maxRetryCount -RetryIntervalSeconds 3
59+
$apiParams = @{
60+
Uri = $repoReleaseUrl
61+
SkipHttpErrorCheck = $true
62+
MaxRetryCount = $maxRetryCount
63+
RetryIntervalSeconds = $retryIntervalSeconds
64+
}
65+
if ($PSBoundParameters.ContainsKey("httpRequestTimeoutSeconds")) {
66+
$apiParams["TimeoutSec"] = $httpRequestTimeoutSeconds
67+
}
68+
$response = Invoke-GitHubApiRequest @apiParams
5269
$releaseData = $response.Result
5370
$statusCode = $response.StatusCode
5471

src/ALZ/Private/Shared/Invoke-GitHubApiRequest.ps1

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
####################################
1+
####################################
22
# Invoke-GitHubApiRequest.ps1 #
33
####################################
44
# Version: 0.1.0
@@ -65,6 +65,9 @@ function Invoke-GitHubApiRequest {
6565
[Parameter(Mandatory = $false, HelpMessage = "If specified, downloads the response to this file path.")]
6666
[string] $OutputFile,
6767

68+
[Parameter(Mandatory = $false, HelpMessage = "Timeout in seconds for the HTTP request.")]
69+
[int] $TimeoutSec,
70+
6871
[Parameter(Mandatory = $false, HelpMessage = "If specified, does not throw on HTTP error status codes.")]
6972
[switch] $SkipHttpErrorCheck
7073
)
@@ -87,49 +90,46 @@ function Invoke-GitHubApiRequest {
8790
Write-Verbose "GitHub CLI is not installed. Proceeding without authentication."
8891
}
8992

90-
$isDownload = -not [string]::IsNullOrEmpty($OutputFile)
91-
$transientStatusCodes = @(408, 429, 500, 502, 503, 504)
92-
$maxAttempts = $MaxRetryCount + 1
93-
94-
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
95-
try {
96-
if ($isDownload) {
97-
Invoke-WebRequest -Uri $Uri -Method $Method -Headers $headers -OutFile $OutputFile -ErrorAction Stop
98-
return
99-
}
100-
101-
if ($SkipHttpErrorCheck) {
102-
$result = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $headers -SkipHttpErrorCheck -StatusCodeVariable "responseStatusCode"
93+
# Build parameters for the generic retry cmdlet
94+
$retryParams = @{
95+
Uri = $Uri
96+
Method = $Method
97+
MaxRetryCount = $MaxRetryCount
98+
RetryIntervalSeconds = $RetryIntervalSeconds
99+
}
103100

104-
$code = [int]$responseStatusCode
101+
if ($PSBoundParameters.ContainsKey("TimeoutSec")) {
102+
$retryParams["TimeoutSec"] = $TimeoutSec
103+
}
105104

106-
if ($code -in $transientStatusCodes -and $attempt -lt $maxAttempts) {
107-
Write-Warning "Request to $Uri returned status $code (attempt $attempt of $maxAttempts). Retrying in $RetryIntervalSeconds seconds..."
108-
Start-Sleep -Seconds $RetryIntervalSeconds
109-
continue
110-
}
105+
if ($headers.Count -gt 0) {
106+
$retryParams["Headers"] = $headers
107+
}
111108

112-
return @{
113-
Result = $result
114-
StatusCode = $code
115-
}
116-
}
109+
# File download — delegate directly
110+
if (-not [string]::IsNullOrEmpty($OutputFile)) {
111+
Invoke-HttpRequestWithRetry @retryParams -OutFile $OutputFile
112+
return
113+
}
117114

118-
return (Invoke-RestMethod -Uri $Uri -Method $Method -Headers $headers -ErrorAction Stop)
119-
} catch {
120-
$responseCode = $null
121-
if ($_.Exception.Response) {
122-
$responseCode = [int]$_.Exception.Response.StatusCode
123-
}
115+
# API call with SkipHttpErrorCheck — parse JSON and return Result/StatusCode hashtable
116+
if ($SkipHttpErrorCheck) {
117+
$response = Invoke-HttpRequestWithRetry @retryParams -SkipHttpErrorCheck -ReturnStatusCode
124118

125-
$isTransient = $responseCode -in $transientStatusCodes
119+
$parsed = $null
120+
if (-not [string]::IsNullOrWhiteSpace($response.Result.Content)) {
121+
$parsed = $response.Result.Content | ConvertFrom-Json
122+
}
126123

127-
if ($isTransient -and $attempt -lt $maxAttempts) {
128-
Write-Warning "Request to $Uri failed with status $responseCode (attempt $attempt of $maxAttempts). Retrying in $RetryIntervalSeconds seconds..."
129-
Start-Sleep -Seconds $RetryIntervalSeconds
130-
} else {
131-
throw
132-
}
124+
return @{
125+
Result = $parsed
126+
StatusCode = $response.StatusCode
133127
}
134128
}
129+
130+
# Standard API call — parse JSON and return the object
131+
$response = Invoke-HttpRequestWithRetry @retryParams
132+
if (-not [string]::IsNullOrWhiteSpace($response.Content)) {
133+
return ($response.Content | ConvertFrom-Json)
134+
}
135135
}

0 commit comments

Comments
 (0)