HTTP Client and Request Handling
The HTTP client layer in glean-powershell is designed as a centralized, low-level abstraction that shields generated cmdlets from the complexities of REST communication, authentication, and error handling. By funneling all network traffic through a small set of private functions, the architecture ensures consistent behavior across all operations, simplifies testing via a single mock point, and guarantees that errors are transformed into structured, terminating PowerShell exceptions. This section details the core components: the raw HTTP wrapper, the request executor that handles URI construction and retries, the query string serializer, the cursor-based pagination engine, and the error resolution logic.
Low-Level HTTP Wrapper
Section titled “Low-Level HTTP Wrapper”The foundation of the HTTP layer is Invoke-GleanHttp, which serves as the single, clean mock point for the entire runtime 1. It is a thin wrapper around PowerShell’s Invoke-RestMethod that normalizes responses into a consistent { StatusCode, Body } object. Crucially, it sets SkipHttpErrorCheck = $true to prevent Invoke-RestMethod from throwing on non-2xx status codes, allowing the caller to handle HTTP errors explicitly. The function accepts standard HTTP parameters including Uri, Method, Headers, Body, ContentType, Form, and WebSession. It dynamically constructs the argument splat for Invoke-RestMethod, ensuring that Form data takes precedence over Body if both are present, and correctly attaches the WebSession if provided.
Core Request Executor
Section titled “Core Request Executor”Invoke-GleanRequest acts as the single HTTP chokepoint for every generated cmdlet 2. It orchestrates the entire request lifecycle: resolving the connection, building the URI, attaching headers, invoking the API, and handling retries or errors.
Connection and Authentication
Section titled “Connection and Authentication”The function first retrieves the connection object via Get-GleanConnectionOrThrow. It explicitly rejects Cookie auth mode for typed REST cmdlets, throwing an InvalidOperationException and directing users to Invoke-GleanWebRequest or token-based auth. For token-based auth, Get-GleanHeaders constructs the Authorization header using a Bearer token derived from the connection’s secure string. It also merges default headers from the connection and any user-provided headers, skipping empty or null values.
URI Construction
Section titled “URI Construction”URIs are built by Get-GleanUri, which combines the base URL, an API-specific prefix (e.g., /rest/api/v1 or /api/index/v1), the path, and query parameters. Path parameters are URL-encoded using [uri]::EscapeDataString and substituted into the path string. The client version is appended as a query parameter if present in the connection object.
Retry Policy
Section titled “Retry Policy”The executor implements an idempotent-only retry policy for specific HTTP status codes. If the Idempotent switch is set and the response is a 429 or 503, it retries up to MaxRetries (default 3) times. The delay uses an exponential backoff strategy, capped at 30 seconds: min(2^attempt, 30). Non-idempotent requests or other error codes bypass retry and go directly to error resolution.
Query String Serialization
Section titled “Query String Serialization”Query parameters are serialized by ConvertTo-GleanQueryString, which respects OpenAPI serialization metadata (style/explode) defined in QueryMeta 3. The default style is form with explode=true.
For scalar values, the function URL-encodes the name and value. For enumerable values (arrays/lists), it checks the explode flag. If explode is true, each item is serialized as a separate key-value pair (e.g., name=a&name=b). If explode is false, items are joined by commas (e.g., name=a,b,c). Boolean values are converted to lowercase strings (true/false) before serialization.
Pagination Handling
Section titled “Pagination Handling”Cursor-based pagination is handled by Invoke-GleanPage, which is invoked when the -All switch is used 4. This function relies on per-operation metadata baked into the cmdlet’s Paginate hashtable, which specifies the request cursor field, response cursor field, a “more” flag, and the item collection field.
The pager works by maintaining a cursor variable, initially null. In each iteration, it adds the cursor to the request body if present, then calls Invoke-GleanRequest. It extracts items from the response using the ItemField and adds them to a collected list. The loop continues as long as the MoreField is true or the ResponseCursor is non-null. The function returns either the raw array of pages, the collected items, or both, depending on the -Raw switch and metadata.
Error Resolution
Section titled “Error Resolution”Non-success HTTP responses are converted into structured, terminating PowerShell errors by Resolve-GleanError 5. It attempts to parse the response body as JSON if it starts with {. If parsing succeeds, it extracts an error message from common fields like errorMessage, message, error, or detail. It also extracts an error code from errorCode, code, or status.
If no message is found in the body, it defaults to a generic message including the HTTP status code. The function creates an ErrorRecord with an HttpRequestException as the inner exception, using a custom error ID (GleanApiError.<StatusCode>) and category InvalidOperation. The error details include the full context: HTTP status, code, message, method, and URI.
# The lowest-level HTTP seam: a thin wrapper over Invoke-RestMethod that always returns a
# { StatusCode, Body } object and never throws on HTTP error status. Isolating the raw call
# here gives the test suite a single, clean mock point for the entire runtime.
function Invoke-GleanHttp {
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory)][string] $Uri,
[Parameter(Mandatory)][string] $Method,
[hashtable] $Headers,
[string] $Body,
[string] $ContentType,
[hashtable] $Form,
[Microsoft.PowerShell.Commands.WebRequestSession] $WebSession
)
$irmArgs = @{
Uri = $Uri
Method = $Method
Headers = $Headers
SkipHttpErrorCheck = $true
StatusCodeVariable = 'statusCode'
ErrorAction = 'Stop'
}
if ($WebSession) { $irmArgs['WebSession'] = $WebSession }
if ($PSBoundParameters.ContainsKey('Form')) { $irmArgs['Form'] = $Form }
elseif ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body) {
$irmArgs['Body'] = $Body
if ($ContentType) { $irmArgs['ContentType'] = $ContentType }
}
$statusCode = 0
$response = Invoke-RestMethod @irmArgs
return [pscustomobject]@{ StatusCode = $statusCode; Body = $response }
}
# The single HTTP chokepoint for every generated cmdlet. Resolves the connection, builds the
# URI (base + api segment + path with encoded path params + query), attaches auth + headers,
# invokes the API, applies an idempotent-only retry policy for 429/503, and converts failures
# into structured terminating errors. Paging is delegated to Invoke-GleanPage when -All is set.
function Invoke-GleanRequest {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory)][ValidateSet('client', 'indexing')][string] $Api,
[Parameter(Mandatory)][string] $Method,
[Parameter(Mandatory)][string] $Path,
[hashtable] $PathParam,
[hashtable] $Query,
[hashtable] $QueryMeta,
[hashtable] $Header,
[object] $Body,
[switch] $Idempotent,
[switch] $Raw,
[switch] $All,
[hashtable] $Paginate,
[int] $MaxRetries = 3
)
$conn = Get-GleanConnectionOrThrow
if ($conn.AuthMode -eq 'Cookie') {
throw [System.InvalidOperationException]::new(
"The typed REST cmdlets are not verified against the web-client API used by cookie/SSO auth. " +
"Use Invoke-GleanWebRequest for cookie mode, or connect with -Token to use the typed cmdlets.")
}
if ($All -and $Paginate) {
return Invoke-GleanPage -Api $Api -Method $Method -Path $Path -PathParam $PathParam `
-Query $Query -QueryMeta $QueryMeta -Header $Header -Body $Body -Paginate $Paginate -Raw:$Raw
}
$uri = Get-GleanUri -Connection $conn -Api $Api -Path $Path -PathParam $PathParam -Query $Query -QueryMeta $QueryMeta
$headers = Get-GleanHeaders -Connection $conn -Header $Header
# Build an RFC3986-encoded query string from a hashtable of values, honoring per-parameter
# OpenAPI serialization metadata (style/explode). Defaults to style=form, explode=true.
function ConvertTo-GleanQueryString {
[CmdletBinding()]
[OutputType([string])]
param(
[hashtable] $Query,
[hashtable] $QueryMeta
)
if ($null -eq $Query -or $Query.Count -eq 0) { return '' }
$pairs = [System.Collections.Generic.List[string]]::new()
foreach ($name in $Query.Keys) {
$value = $Query[$name]
if ($null -eq $value) { continue }
$meta = if ($QueryMeta -and $QueryMeta.ContainsKey($name)) { $QueryMeta[$name] } else { $null }
$style = if ($meta -and $meta.style) { $meta.style } else { 'form' }
# explode defaults to $true for form style; honor an explicit $false.
$explode = $true
if ($meta -and $meta.ContainsKey('explode') -and $null -ne $meta.explode) { $explode = [bool]$meta.explode }
$encName = [uri]::EscapeDataString($name)
if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) {
$items = @($value | ForEach-Object { _ConvertGleanScalar $_ })
if ($explode) {
foreach ($item in $items) {
$pairs.Add("$encName=$([uri]::EscapeDataString($item))")
}
}
else {
# style=form, explode=false -> name=a,b,c
$joined = ($items | ForEach-Object { [uri]::EscapeDataString($_) }) -join ','
$pairs.Add("$encName=$joined")
}
}
else {
# Cursor pager for the handful of cursor-paginated client operations. Driven entirely by the
# per-operation metadata the generator baked into each cmdlet (request/response cursor field,
# stop signal, and item-collection field) -- never a generic guess.
function Invoke-GleanPage {
[CmdletBinding()]
[OutputType([object])]
param(
[string] $Api, [string] $Method, [string] $Path,
[hashtable] $PathParam, [hashtable] $Query, [hashtable] $QueryMeta, [hashtable] $Header,
[object] $Body, [hashtable] $Paginate, [switch] $Raw
)
$reqCursor = $Paginate.RequestCursor
$respCursor = $Paginate.ResponseCursor
$moreField = $Paginate.MoreField
$itemField = $Paginate.ItemField
# Work on a copy of the body so the caller's hashtable is not mutated across pages.
$body = @{}
if ($Body) { foreach ($k in $Body.Keys) { $body[$k] = $Body[$k] } }
$collected = [System.Collections.Generic.List[object]]::new()
$pages = [System.Collections.Generic.List[object]]::new()
$cursor = $null
while ($true) {
if ($cursor) { $body[$reqCursor] = $cursor }
$page = Invoke-GleanRequest -Api $Api -Method $Method -Path $Path -PathParam $PathParam `
-Query $Query -QueryMeta $QueryMeta -Header $Header -Body $body -Idempotent
$pages.Add($page)
if ($itemField -and $null -ne $page.$itemField) {
foreach ($item in @($page.$itemField)) { $collected.Add($item) }
}
$cursor = $page.$respCursor
$hasMore = if ($moreField) { [bool]$page.$moreField } else { [bool]$cursor }
if (-not $hasMore -or -not $cursor) { break }
# Turn a non-success HTTP response into a structured, terminating PowerShell error.
function Resolve-GleanError {
[CmdletBinding()]
param(
[int] $StatusCode,
[object] $ResponseBody,
[string] $Method,
[string] $Uri
)
$code = $null
$message = $null
$body = $ResponseBody
if ($ResponseBody -is [string] -and $ResponseBody.Trim().StartsWith('{')) {
try { $body = $ResponseBody | ConvertFrom-Json -ErrorAction Stop } catch { $body = $ResponseBody }
}
if ($body -is [psobject]) {
foreach ($p in 'errorMessage', 'message', 'error', 'detail') {
if ($body.PSObject.Properties.Name -contains $p -and $body.$p) { $message = [string]$body.$p; break }
}
foreach ($p in 'errorCode', 'code', 'status') {
if ($body.PSObject.Properties.Name -contains $p -and $body.$p) { $code = [string]$body.$p; break }
}
}
if (-not $message) { $message = "Glean API request failed with HTTP $StatusCode." }
$detail = "Glean API error (HTTP $StatusCode$(if ($code) { " / $code" })): $message [$Method $Uri]"
$err = [System.Management.Automation.ErrorRecord]::new(
[System.Net.Http.HttpRequestException]::new($detail),
"GleanApiError.$StatusCode",
[System.Management.Automation.ErrorCategory]::InvalidOperation,
$Uri)
$err.ErrorDetails = [System.Management.Automation.ErrorDetails]::new($detail)
throw $err
}