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)
        }
    }
}