Private/Invoke-GitHubConditionalGet.ps1
|
# --------------------------------------------------------------------------- # Invoke-GitHubConditionalGet # A single GET issued as a conditional request: sends the cached ETag as # 'If-None-Match', replays the cached payload when GitHub answers 304 Not # Modified, and refreshes the cache entry when it answers 200. # # This exists to make polling affordable. GitHub does not charge a 304 # against the hourly rate limit, so a dashboard watching an idle fleet - # where the runner list and the queue are identical tick after tick - costs # effectively nothing, and the budget stays available for the ticks where # something is actually happening. # # Returns an object with two members so a caller gets the data and the # budget reading from one call site: # .Value - the parsed response body (cached payload on a 304) # .RateLimit - the X-RateLimit-* reading, or $null if GitHub sent none # # Only responses that carry an ETag are cached: without one the entry could # never be revalidated, and a payload that can never be proven current is # worse than no cache at all. # # Private to the module - not exported. # --------------------------------------------------------------------------- function Invoke-GitHubConditionalGet { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Token, # Path relative to https://api.github.com/. Doubles as the cache key, # so two calls that differ only by query string cache independently. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Endpoint, # Conditional-request cache, MUTATED IN PLACE. Omit it (or pass $null) # to make every call unconditional. [Parameter()] [AllowNull()] [hashtable] $Cache ) $httpNotModified = 304 $httpLowestSuccess = 200 $httpLowestFailure = 300 $requestHeader = New-GitHubRequestHeader $cached = if ($null -ne $Cache -and $Cache.ContainsKey($Endpoint)) { $Cache[$Endpoint] } else { $null } if ($null -ne $cached) { $requestHeader['If-None-Match'] = $cached.ETag } $response = Invoke-GitHubApi ` -Token $Token ` -Endpoint $Endpoint ` -Header $requestHeader ` -IncludeResponseDetail $body = $response.Content $responseHeader = $response.Headers $statusCode = $response.StatusCode # Read the budget before any early return - a 304 reports it too, and it # is the reading a caller most wants when it is polling hard. $remaining = Get-HttpHeaderValue $responseHeader 'X-RateLimit-Remaining' $rateLimit = if ($null -ne $remaining) { $limit = Get-HttpHeaderValue $responseHeader 'X-RateLimit-Limit' $reset = Get-HttpHeaderValue $responseHeader 'X-RateLimit-Reset' [PSCustomObject]@{ Remaining = [int] $remaining Limit = if ($null -ne $limit) { [int] $limit } else { 0 } ResetsAt = if ($null -ne $reset) { [DateTimeOffset]::FromUnixTimeSeconds([long] $reset).UtcDateTime } else { $null } } } else { $null } if ($statusCode -eq $httpNotModified) { # Only reachable when we sent an If-None-Match, so $cached is set. The # guard covers a server that returns 304 unbidden - replaying a # payload we do not have would surface as a confusing null downstream. if ($null -eq $cached) { throw "GitHub API GET $Endpoint returned HTTP $httpNotModified with no cached payload to replay." } return [PSCustomObject]@{ Value = $cached.Payload; RateLimit = $rateLimit } } # -StatusCodeVariable turns off Invoke-RestMethod's own error check, so # every non-success status has to be raised here instead. if ($statusCode -lt $httpLowestSuccess -or $statusCode -ge $httpLowestFailure) { throw "GitHub API GET $Endpoint failed with HTTP $statusCode." } $etag = Get-HttpHeaderValue $responseHeader 'ETag' if ($null -ne $Cache -and $etag) { $Cache[$Endpoint] = [PSCustomObject]@{ ETag = $etag; Payload = $body } } [PSCustomObject]@{ Value = $body; RateLimit = $rateLimit } } |