functions/github/Invoke-GitHubRestMethod.ps1
function Invoke-GitHubRestMethod { [CmdletBinding()] param ( [Parameter(Mandatory=$True)] [uri] $Uri, [Parameter()] [string] $Verb = 'GET', [Parameter()] [hashtable] $Body, [Parameter()] [string] $Token = $env:GITHUB_TOKEN, [Parameter()] [hashtable] $Headers = @{}, [Parameter()] [int[]] $HttpErrorStatusCodesToIgnore = @(), [Parameter()] [switch] $AllPages, [Parameter()] [int] $MaxRetries = 5, [Parameter()] [float] $RetryBackOffBaseFactor = 1.5, [Parameter()] [int] $InitialBackOffSeconds = 60 ) if ($Headers.Keys -notcontains 'Accept') { $Headers += @{ Accept = 'application/vnd.github.machine-man-preview+json' } } if ($Headers.Keys -notcontains 'Authorization') { $Headers += @{ Authorization = "Token $Token" } } if ($Headers.Keys -notcontains 'Content-Type') { $Headers += @{ "Content-Type" = "application/json" } } $succeeded = $false $retryCount = 0 do { try { $resp = Invoke-RestMethod -Headers $Headers ` -Method $Verb ` -Uri $Uri ` -Body ($Body|ConvertTo-Json -Depth 100 -Compress) ` -FollowRelLink:$AllPages $succeeded = $true } catch { if ( $_.Exception.Response.StatusCode -eq 429 -or ` ($_.Exception.Response.StatusCode -eq 403 -and $_.Exception.Message -match 'rate limit') ) { # Handle rate limit errors as per GitHub's API guidelines # ref: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28 # 1. Check for 429 or 403 with 'rate limit' in the error message # 2. Check for 'Retry-After' header # 3. Check for 'X-RateLimit-Remaining' = 0, wait until 'X-RateLimit-Reset' header # 4. Wait for 60 seconds, expoenentially backing off for subsequent retries $retryAfter = $_.Exception.Response.Headers.Contains("Retry-After") ? ($_.Exception.Response.Headers.GetValues("Retry-After")[0]) : $null if ($retryAfter) { Write-Host "Rate limit exceeded. Waiting for $retryAfter seconds..." } elseif ( $_.Exception.Response.Headers.Contains('X-RateLimit-Remaining') -and ` $_.Exception.Response.Headers.GetValues('X-RateLimit-Remaining')[0] -eq 0 -and ` $_.Exception.Response.Headers.Contains('X-RateLimit-Reset') ) { $RateLimitReset = [datetime]::FromFileTimeUtc($_.Exception.Response.Headers.GetValues('X-RateLimit-Reset')[0]) $retryAfter = ($RateLimitReset - [datetime]::UtcNow).TotalSeconds Write-Host "Rate limit exceeded. Waiting for quota reset in $retryAfter seconds..." } else { # We have hit a secondary rate limit with no retry context $retryAfter = [math]::Pow($RetryBackOffBaseFactor, $retryCount) * $InitialBackOffSeconds Write-Host "Rate limit exceeded. Exponentional back-off, waiting for $retryAfter seconds..." } Start-Sleep -Seconds $retryAfter } elseif ($_.Exception.Response.StatusCode -notin $HttpErrorStatusCodesToIgnore) { throw $_ } $retryCount++ } } while (!$succeeded -and $retryCount -le $MaxRetries) # Flatten the result set in the event we traversed multiple pages $resp | ForEach-Object { $_ } } |