Skip to content

Commit d74249a

Browse files
author
rvdwegen
committed
this probably needs cleanup
1 parent 5a82592 commit d74249a

File tree

1 file changed

+132
-19
lines changed

1 file changed

+132
-19
lines changed

Modules/CIPPCore/Public/GraphHelper/New-CIPPAzRestRequest.ps1

Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ function New-CIPPAzRestRequest {
3535
Use basic parsing (for older PowerShell versions)
3636
.PARAMETER WebSession
3737
Web session object for maintaining cookies/state
38+
.PARAMETER MaxRetries
39+
Maximum number of retry attempts for transient failures. Defaults to 3.
3840
.EXAMPLE
3941
New-CIPPAzRestRequest -Uri 'https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{name}?api-version=2020-06-01'
4042
Gets Azure Resource Manager resource using managed identity
@@ -92,7 +94,10 @@ function New-CIPPAzRestRequest {
9294
[switch]$UseBasicParsing,
9395

9496
[Parameter(Mandatory = $false)]
95-
[Microsoft.PowerShell.Commands.WebRequestSession]$WebSession
97+
[Microsoft.PowerShell.Commands.WebRequestSession]$WebSession,
98+
99+
[Parameter(Mandatory = $false)]
100+
[int]$MaxRetries = 3
96101
)
97102

98103
# Get Azure Managed Identity token
@@ -178,32 +183,140 @@ function New-CIPPAzRestRequest {
178183
$RestMethodParams['WebSession'] = $WebSession
179184
}
180185

181-
# Invoke the REST method
182-
try {
183-
$Response = Invoke-RestMethod @RestMethodParams
186+
# Invoke the REST method with retry logic
187+
$RetryCount = 0
188+
$RequestSuccessful = $false
189+
$Message = $null
190+
$MessageObj = $null
184191

185-
# For compatibility with Invoke-AzRestMethod behavior, return object with Content property if response is a string
186-
# Otherwise return the parsed object directly
187-
if ($Response -is [string]) {
188-
return [PSCustomObject]@{
189-
Content = $Response
192+
Write-Information "$($Method.ToUpper()) [ $Uri ] | attempt: $($RetryCount + 1) of $MaxRetries"
193+
194+
do {
195+
try {
196+
$Response = Invoke-RestMethod @RestMethodParams
197+
$RequestSuccessful = $true
198+
199+
# For compatibility with Invoke-AzRestMethod behavior, return object with Content property if response is a string
200+
# Otherwise return the parsed object directly
201+
if ($Response -is [string]) {
202+
return [PSCustomObject]@{
203+
Content = $Response
204+
}
190205
}
191-
}
192206

193-
return $Response
194-
} catch {
195-
$errorMessage = "Azure REST API call failed: $($_.Exception.Message)"
196-
if ($_.Exception.Response) {
207+
return $Response
208+
} catch {
209+
$ShouldRetry = $false
210+
$WaitTime = 0
211+
212+
# Extract error message from JSON response if available
197213
try {
198-
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
199-
$responseBody = $reader.ReadToEnd()
200-
$reader.Close()
201-
$errorMessage += "`nResponse: $responseBody"
214+
if ($_.ErrorDetails.Message) {
215+
$MessageObj = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue
216+
if ($MessageObj.error) {
217+
$MessageObj | Add-Member -NotePropertyName 'url' -NotePropertyValue $Uri -Force
218+
$Message = if ($MessageObj.error.message) {
219+
Get-NormalizedError -message $MessageObj.error.message
220+
} elseif ($MessageObj.error.code) {
221+
$MessageObj.error.code
222+
} else {
223+
$_.Exception.Message
224+
}
225+
} else {
226+
$Message = Get-NormalizedError -message $_.ErrorDetails.Message
227+
}
228+
} else {
229+
$Message = $_.Exception.Message
230+
}
202231
} catch {
203-
# Ignore errors reading response stream
232+
$Message = $_.Exception.Message
233+
}
234+
235+
# If we couldn't extract a message, use the exception message
236+
if ([string]::IsNullOrEmpty($Message)) {
237+
$Message = $_.Exception.Message
238+
$MessageObj = @{
239+
error = @{
240+
code = $_.Exception.GetType().FullName
241+
message = $Message
242+
url = $Uri
243+
}
244+
}
245+
}
246+
247+
# Check for 429 Too Many Requests (rate limiting)
248+
if ($_.Exception.Response -and $_.Exception.Response.StatusCode -eq 429) {
249+
$RetryAfterHeader = $_.Exception.Response.Headers['Retry-After']
250+
if ($RetryAfterHeader) {
251+
$WaitTime = [int]$RetryAfterHeader
252+
Write-Warning "Rate limited (429). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries"
253+
$ShouldRetry = $true
254+
} elseif ($RetryCount -lt $MaxRetries) {
255+
# Exponential backoff if no Retry-After header
256+
$WaitTime = [Math]::Min([Math]::Pow(2, $RetryCount), 60) # Cap at 60 seconds
257+
Write-Warning "Rate limited (429) without Retry-After header. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries"
258+
$ShouldRetry = $true
259+
}
260+
}
261+
# Check for 503 Service Unavailable or temporary errors
262+
elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode -eq 503) {
263+
if ($RetryCount -lt $MaxRetries) {
264+
$WaitTime = Get-Random -Minimum 1.1 -Maximum 3.1 # Random sleep between 1-3 seconds
265+
Write-Warning "Service unavailable (503). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries"
266+
$ShouldRetry = $true
267+
}
268+
}
269+
# Check for "Resource temporarily unavailable" or other transient errors
270+
elseif ($Message -like '*Resource temporarily unavailable*' -or $Message -like '*temporarily*' -or $Message -like '*timeout*') {
271+
if ($RetryCount -lt $MaxRetries) {
272+
$WaitTime = Get-Random -Minimum 1.1 -Maximum 3.1 # Random sleep between 1-3 seconds
273+
Write-Warning "Transient error detected. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries"
274+
$ShouldRetry = $true
275+
}
276+
}
277+
# Check for 500/502/504 server errors (retryable)
278+
elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode -in @(500, 502, 504)) {
279+
if ($RetryCount -lt $MaxRetries) {
280+
$WaitTime = Get-Random -Minimum 1.1 -Maximum 3.1 # Random sleep between 1-3 seconds
281+
Write-Warning "Server error ($($_.Exception.Response.StatusCode)). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries"
282+
$ShouldRetry = $true
283+
}
284+
}
285+
286+
# Retry if conditions are met
287+
if ($ShouldRetry -and $RetryCount -lt $MaxRetries) {
288+
$RetryCount++
289+
if ($WaitTime -gt 0) {
290+
Start-Sleep -Seconds $WaitTime
291+
}
292+
Write-Information "$($Method.ToUpper()) [ $Uri ] | attempt: $($RetryCount + 1) of $MaxRetries"
293+
} else {
294+
# Final failure - build detailed error message
295+
$errorMessage = "Azure REST API call failed: $Message"
296+
if ($_.Exception.Response) {
297+
$errorMessage += " (Status: $($_.Exception.Response.StatusCode))"
298+
try {
299+
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
300+
$responseBody = $reader.ReadToEnd()
301+
$reader.Close()
302+
if ($responseBody) {
303+
$errorMessage += "`nResponse: $responseBody"
304+
}
305+
} catch {
306+
# Ignore errors reading response stream
307+
}
308+
}
309+
$errorMessage += "`nURI: $Uri"
310+
311+
Write-Error -Message $errorMessage -ErrorAction $ErrorActionPreference
312+
return
204313
}
205314
}
315+
} while (-not $RequestSuccessful -and $RetryCount -le $MaxRetries)
206316

317+
# Should never reach here, but just in case
318+
if (-not $RequestSuccessful) {
319+
$errorMessage = "Azure REST API call failed after $MaxRetries attempts: $Message`nURI: $Uri"
207320
Write-Error -Message $errorMessage -ErrorAction $ErrorActionPreference
208321
return
209322
}

0 commit comments

Comments
 (0)