@@ -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 += " `n Response: $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 += " `n Response: $responseBody "
304+ }
305+ } catch {
306+ # Ignore errors reading response stream
307+ }
308+ }
309+ $errorMessage += " `n URI: $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 `n URI: $Uri "
207320 Write-Error - Message $errorMessage - ErrorAction $ErrorActionPreference
208321 return
209322 }
0 commit comments