Private/Invoke-WithRetry.ps1
|
function Invoke-WithRetry { <# Run a PnP call, and if SharePoint throttles it (429 / 503), wait and try again. Tenant-wide auditing makes thousands of calls; without this, the first throttle ends the run. Honours the server's Retry-After header when it is present - that is the wait SharePoint is actually asking for. Falls back to exponential backoff otherwise. Anything that is NOT a throttle is rethrown immediately, because retrying a 404 or an auth failure just wastes time. #> param( [Parameter(Mandatory)] [scriptblock] $Action, [int] $MaxAttempts = 5, [string] $Because = 'call' ) for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { try { return & $Action } catch { $ex = $_.Exception $msg = $ex.Message # Pull an HTTP status if one is reachable on the exception chain. $status = $null $probe = $ex while ($probe -and -not $status) { if ($probe.PSObject.Properties['Response'] -and $probe.Response) { # Not every exception's Response exposes a numeric StatusCode; # if this one does not, fall through to message matching. try { $status = [int]$probe.Response.StatusCode } catch { Write-Debug "no StatusCode on this response" } } $probe = $probe.InnerException } $isThrottle = $status -in 429, 503 -or $msg -match '(?i)\b429\b|too many requests|throttl|\b503\b|service unavailable' if (-not $isThrottle -or $attempt -eq $MaxAttempts) { throw } # Prefer the server's Retry-After; otherwise back off exponentially. $wait = $null $probe = $ex while ($probe -and -not $wait) { if ($probe.PSObject.Properties['Response'] -and $probe.Response) { try { $ra = $probe.Response.Headers['Retry-After'] if ($ra) { $wait = [int]$ra } } catch { Write-Debug "no Retry-After header available" } } $probe = $probe.InnerException } if (-not $wait) { $wait = [Math]::Min([Math]::Pow(2, $attempt), 60) } Write-Warning "Throttled on $Because (attempt $attempt/$MaxAttempts). Waiting $wait s." Start-Sleep -Seconds $wait } } } |