Private/Get-AACErrorMessage.ps1

function Get-AACErrorMessage {
    <#
    .SYNOPSIS
        Azure's own reason from an error response body - error.message, the
        nested innererror detail the query APIs give and the details list
        Resource Graph gives (where its real reason is: the top message is
        only "Please provide below info when asking for support"), or a
        top-level message; Azure Storage's XML error as 'Code: message' -
        else -Fallback.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [AllowEmptyString()]
        [string] $Content,

        [string] $Fallback
    )

    # Azure Storage answers in XML: <Error><Code>..</Code><Message>..
    # RequestId:..</Message></Error>. The code goes first - it says what to
    # do (AuthorizationPermissionMismatch: a data role is missing).
    if ($Content -and $Content.TrimStart([char]0xFEFF, ' ', "`r", "`n", "`t").StartsWith('<')) {
        $code = if ($Content -match '<Code>([^<]*)</Code>') { $Matches[1].Trim() } else { '' }
        $text = if ($Content -match '(?s)<Message>([^<]*)</Message>') { ($Matches[1] -split '\r?\n')[0].Trim() } else { '' }
        $text = [System.Net.WebUtility]::HtmlDecode($text)
        if ($code -and $text) { return "${code}: $text" }
        if ($code -or $text) { return "$code$text" }
    }
    if ($Content) {
        try {
            $parsed = ConvertFrom-Json -InputObject $Content -AsHashtable -ErrorAction Stop
            if ($parsed -is [System.Collections.IDictionary] -and $parsed['error'] -is [System.Collections.IDictionary] -and $parsed['error']['message']) {
                $messages = [System.Collections.Generic.List[string]]::new()
                $support = [System.Collections.Generic.List[string]]::new()
                # The error, its innererror chain, and its details - each of
                # which can have its own - depth first.
                $pending = [System.Collections.Generic.Stack[object]]::new()
                $pending.Push($parsed['error'])
                while ($pending.Count) {
                    $level = $pending.Pop()
                    if ($level -isnot [System.Collections.IDictionary]) { continue }
                    $text = ([string]$level['message']).Trim()
                    # Resource Graph's "Please provide below info..." is for a
                    # support call, not the reason: last.
                    if ($text -match '^Please provide below info') { if (-not $support.Contains($text)) { $support.Add($text) } }
                    elseif ($text -and -not $messages.Contains($text)) { $messages.Add($text) }
                    $children = @(@($level['details']) + @($level['innererror']) | Where-Object { $_ -is [System.Collections.IDictionary] })
                    for ($i = $children.Count - 1; $i -ge 0; $i--) { $pending.Push($children[$i]) }
                }
                return (@($messages) + @($support)) -join ' '
            }
            if ($parsed -is [System.Collections.IDictionary] -and $parsed['message']) {
                return [string]$parsed['message']
            }
        }
        catch { Write-Debug "The error body is not JSON: $Content" }
    }
    $Fallback
}