private/Invoke-UnraidQuery.ps1
|
function Invoke-UnraidQuery { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Query, [Parameter()] [UnraidSession]$Session = $script:DefaultUnraidSession ) process { if (!$Session) { throw [UnraidSessionException]::new("No active Unraid session. Run Connect-Unraid first.") } Write-Verbose "Executing GraphQL query against $($Session.Uri)" $headers = @{} if ($Session.AuthType -eq 'ApiKey') { $headers["x-api-key"] = $Session.ApiKey } else { $headers["x-csrf-token"] = $Session.CsrfToken } $requestBody = @{ query = $Query } | ConvertTo-Json -Depth 10 $requestParams = @{ Uri = $Session.Uri Method = "Post" Body = $requestBody Headers = $headers ContentType = "application/json" ErrorAction = "Stop" } if ($Session.WebSession) { $requestParams['WebSession'] = $Session.WebSession } if ($Session.SkipCertificateCheck -and ($PSVersionTable.PSVersion.Major -ge 6)) { $requestParams['SkipCertificateCheck'] = $true } try { Write-Debug "Sending POST request..." $response = Invoke-WebRequest @requestParams -UseBasicParsing # Had some issues using PS 5.1's ConvertFrom-Json with GraphQL's reserved keyword "__type" # This sanitizes that before displaying $rawContent = $response.Content if ($rawContent -match '"__type"\s*:') { Write-Debug "Sanitizing '__type' reserved keyword." $rawContent = $rawContent -replace '"__type"\s*:', '"schemaType":' } try { $json = $rawContent | ConvertFrom-Json } catch { $snippet = if ($rawContent.Length -gt 100) { $rawContent.Substring(0, 100) + "..." } else { $rawContent } throw [UnraidApiException]::new("Server returned 200 OK but content was not valid JSON. Content snippet: $snippet") } if ($json.errors) { $errorMessages = $json.errors | ForEach-Object { $_.message } $isSyntaxError = $json.errors | Where-Object { $_.message -match "Syntax Error" -or $_.message -match "Cannot query field" } if ($isSyntaxError) { $formattedErrors = $json.errors | ConvertTo-Json -Depth 5 throw [UnraidApiException]::new("GraphQL Syntax/Schema Error.`nQuery: $Query`nErrors: $formattedErrors") } elseif ($errorMessages -match "not found" -or $errorMessages -match "already exists" -or $errorMessages -match "invalid") { throw [UnraidApiException]::new(($errorMessages -join "; ")) } else { Write-Debug "Full Error Payload: $($json.errors | ConvertTo-Json -Depth 5)" throw [UnraidApiException]::new("Unraid API Error: " + ($errorMessages -join "; ")) } } return $json.data } catch { # Re-throw our own custom exceptions if ($_.Exception -is [UnraidApiException] -or $_.Exception -is [UnraidSessionException]) { throw $_ } $ex = $_.Exception $responseBody = $null $statusCode = 0 $extractionError = $null # Extract details from the exception for further details try { # PScore / 6+ (HttpResponseException) if ($ex.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException' -and $ex.Response) { $statusCode = [int]$ex.Response.StatusCode $responseBody = $ex.Response.Content.ReadAsStringAsync().Result } # PS5.1 (WebException) elseif ($ex -is [System.Net.WebException] -and $ex.Response) { if ($ex.Response.StatusCode) { $statusCode = [int]$ex.Response.StatusCode } $stream = $ex.Response.GetResponseStream() if ($stream) { if ($stream.CanSeek) { $stream.Position = 0 } # Try to rewind if possible $reader = [System.IO.StreamReader]::new($stream) $responseBody = $reader.ReadToEnd() $reader.Dispose() } } } catch { $extractionError = $_.Exception.Message Write-Debug "Failed to extract error body: $extractionError" } # Status code specific handling - translate to friendly text switch ($statusCode) { 401 { throw [UnraidSessionException]::new("Authentication failed (HTTP 401). Session expired.", $ex) } 403 { throw [UnraidSessionException]::new("Access denied (HTTP 403). Permission denied.", $ex) } 502 { throw [UnraidApiException]::new("Unraid API unavailable (HTTP 502). Service may be restarting.", $ex) } 504 { throw [UnraidApiException]::new("Unraid API timed out (HTTP 504).", $ex) } } # Parse the returned json - we don't really care for this to be prettied up, the raw message is useful for debug if (![string]::IsNullOrWhiteSpace($responseBody)) { try { $jsonError = $responseBody | ConvertFrom-Json if ($jsonError.errors) { $msgs = $jsonError.errors | ForEach-Object { $_.message } # Syntax error handiling if ($msgs -match "Syntax Error" -or $msgs -match "Cannot query field") { $formattedErrors = $jsonError.errors | ConvertTo-Json -Depth 5 throw [UnraidApiException]::new("GraphQL Syntax Error (HTTP $statusCode).`nQuery: $Query`nErrors: $formattedErrors", $ex) } # Return just the cleaned up message from the API throw [UnraidApiException]::new(($msgs -join "; "), $ex) } elseif ($jsonError.message) { throw [UnraidApiException]::new("API Error: $($jsonError.message)", $ex) } } catch [UnraidApiException] { throw $_ } catch { # json parse failed, show raw body $cleanBody = $responseBody -replace "[\r\n]+", " " $shortBody = if ($cleanBody.Length -gt 200) { $cleanBody.Substring(0, 200) + "..." } else { $cleanBody } throw [UnraidApiException]::new("Server returned HTTP $($statusCode): $shortBody", $ex) } } # If we landed here, we have a status code but failed to get a body if ($statusCode -gt 0) { $msg = "HTTP request failed with code $statusCode." if ($extractionError) { $msg += " (Could not read error body: $extractionError)" } throw [UnraidApiException]::new($msg, $ex) } throw [UnraidApiException]::new("Unknown Error: $($ex.Message)", $ex) } } } |