Private/Resolve-TBErrorResponse.ps1

function Resolve-TBErrorResponse {
    <#
    .SYNOPSIS
        Parses a Microsoft Graph error response into a structured object.
    .DESCRIPTION
        Takes a Graph API error (typically from a catch block) and extracts
        the error code, message, and request ID for consistent error reporting.
        Checks multiple sources for the API error body: ErrorDetails.Message,
        the HttpResponseMessage content stream, and the exception message itself.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$ErrorRecord,

        [Parameter()]
        [string]$ResponseBody
    )

    $result = [PSCustomObject]@{
        StatusCode = $null
        ErrorCode  = $null
        Message    = $null
        RequestId  = $null
        RawError   = $null
    }

    try {
        if ($ErrorRecord -is [System.Management.Automation.ErrorRecord]) {
            $exception = $ErrorRecord.Exception
            $result.RawError = $ErrorRecord.ToString()

            # Collect all potential JSON sources in priority order
            $jsonSources = @()

            # Source 1: Explicit ResponseBody parameter (pre-captured by caller)
            if ($ResponseBody) {
                $jsonSources += $ResponseBody
            }

            # Source 2: ErrorDetails.Message (PowerShell error details, sometimes has API body)
            if ($ErrorRecord.ErrorDetails -and $ErrorRecord.ErrorDetails.Message) {
                $edm = $ErrorRecord.ErrorDetails.Message
                # The Graph SDK may return the full HTTP response (headers + body) in
                # ErrorDetails.Message. Extract the body after the blank line separator
                # so we try clean JSON first, before falling back to the raw text.
                if ($edm -match '(\r?\n){2}') {
                    $parts = $edm -split '(?:\r?\n){2}', 2
                    if ($parts.Count -eq 2) {
                        $httpBody = $parts[1].Trim()
                        if ($httpBody) {
                            $jsonSources += $httpBody
                        }
                    }
                }
                $jsonSources += $edm
            }

            # Source 3: Exception message (Graph errors sometimes embed JSON)
            $messageText = $exception.Message
            if ($messageText) {
                $jsonSources += $messageText
            }

            # Try each source: first attempt direct JSON parse, then regex extraction
            foreach ($source in $jsonSources) {
                if ($result.ErrorCode) { break }

                # Attempt 1: Direct ConvertFrom-Json (handles clean JSON strings)
                try {
                    $parsed = $source | ConvertFrom-Json
                    if ($parsed.error -and $parsed.error.code) {
                        $result.ErrorCode = $parsed.error.code
                        $result.Message = $parsed.error.message
                        if ($parsed.error.innerError -and $parsed.error.innerError.'request-id') {
                            $result.RequestId = $parsed.error.innerError.'request-id'
                        }
                        break
                    }
                    elseif ($parsed.code) {
                        # Flat error format (no "error" wrapper)
                        $result.ErrorCode = $parsed.code
                        $result.Message = $parsed.message
                        break
                    }
                }
                catch {
                    # Not valid JSON on its own, try regex extraction
                }

                # Attempt 2: Regex extraction for JSON embedded in other text
                if ($source -match '\{.*"error".*\}') {
                    $jsonMatch = $source | Select-String -Pattern '\{.*\}' | ForEach-Object { $_.Matches[0].Value }
                    if ($jsonMatch) {
                        try {
                            $parsed = $jsonMatch | ConvertFrom-Json
                            if ($parsed.error) {
                                $result.ErrorCode = $parsed.error.code
                                $result.Message = $parsed.error.message
                                if ($parsed.error.innerError -and $parsed.error.innerError.'request-id') {
                                    $result.RequestId = $parsed.error.innerError.'request-id'
                                }
                                break
                            }
                        }
                        catch {
                            # Regex matched but JSON parse failed
                        }
                    }
                }
            }

            if (-not $result.Message) {
                $result.Message = $messageText
            }

            # Try to extract status code
            if ($exception.PSObject.Properties['StatusCode']) {
                $result.StatusCode = [int]$exception.StatusCode
            }
            elseif ($exception.PSObject.Properties['Response'] -and $exception.Response.PSObject.Properties['StatusCode']) {
                $result.StatusCode = [int]$exception.Response.StatusCode
            }
        }
        else {
            $result.Message = $ErrorRecord.ToString()
            $result.RawError = $ErrorRecord.ToString()
        }
    }
    catch {
        $result.Message = $ErrorRecord.ToString()
        $result.RawError = $ErrorRecord.ToString()
    }

    return $result
}