Providers/PingOne/Private/Get-PingOneErrorDetail.ps1

function Get-PingOneErrorDetail {
    <#
    .SYNOPSIS
        Pulls the code, message and correlation id out of a failed PingOne response

    .DESCRIPTION
        PingOne answers a failure with a body rather than a useful status code. A 400 is
        returned for a malformed request, an unsupported grant type, a duplicate name and a
        field that failed validation alike, so branching on the status alone cannot tell a
        caller which of those happened. The body carries what matters:

            {
              "id": "...", "code": "INVALID_DATA", "message": "The request could not be completed.",
              "details": [ { "code": "INVALID_VALUE", "target": "name", "message": "..." } ]
            }

        The `id` is the correlation id, and it is the only thing Ping's support can act on, so
        it is kept even though nothing in this module reads it.

        Reading the body is version-dependent, which is the reason this is its own function.
        PowerShell 7 puts it in ErrorDetails.Message. Windows PowerShell 5.1 usually does too,
        but leaves it empty for some failures and only exposes the body on the exception's
        response stream - which can be read exactly once, so a caller that peeks at it before
        calling here gets nothing back from here.

    .PARAMETER ErrorRecord
        The error record from the failed call.

    .OUTPUTS
        PSCustomObject with Status, Code, Message, Target, CorrelationId and Summary. Summary is
        the one-line form worth putting in an exception message.

    .EXAMPLE
        PS> try { Invoke-WebRequest ... } catch { Get-PingOneErrorDetail -ErrorRecord $_ }

        DESCRIPTION: Turns a failure into something worth printing
        OUTPUT: Status 400, Code INVALID_DATA, Target name, and a summary naming all three
        USE CASE: Called by Invoke-PingOneRequest on every failure

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    $status = 0
    if ($ErrorRecord.Exception.Response) {
        try { $status = [int]$ErrorRecord.Exception.Response.StatusCode } catch { $status = 0 }
    }

    $raw = $ErrorRecord.ErrorDetails.Message

    # Windows PowerShell 5.1 leaves the body on the response stream for some failures. The
    # stream can only be read once, so this is the only place that reads it.
    if ([string]::IsNullOrWhiteSpace($raw) -and $ErrorRecord.Exception.Response) {
        try {
            $stream = $ErrorRecord.Exception.Response.GetResponseStream()
            if ($stream) {
                $reader = New-Object System.IO.StreamReader($stream)
                try { $raw = $reader.ReadToEnd() } finally { $reader.Dispose() }
            }
        }
        catch {
            Write-Verbose "Could not read the error body from the response stream: $($_.Exception.Message)"
        }
    }

    $code = $null
    $message = $null
    $target = $null
    $correlationId = $null

    if (-not [string]::IsNullOrWhiteSpace($raw)) {
        try {
            $parsed = $raw | ConvertFrom-Json
            $code = $parsed.code
            $message = $parsed.message
            $correlationId = $parsed.id
            if ($parsed.details) {
                $first = @($parsed.details)[0]
                $target = $first.target
                # The detail is more specific than the top-level message, which is usually the
                # useless "The request could not be completed."
                if ($first.message) { $message = $first.message }
            }
        }
        catch {
            Write-Verbose 'The error body was not JSON; using it as the message.'
            $message = ($raw -replace '\s+', ' ')
        }
    }

    if (-not $message) { $message = $ErrorRecord.Exception.Message }

    $summary = $code
    if ($target) { $summary = '{0} on {1}' -f $summary, $target }
    if ($summary) { $summary = '{0}: {1}' -f $summary, $message } else { $summary = $message }
    if ($correlationId) { $summary = '{0} (correlation id {1})' -f $summary, $correlationId }

    return [PSCustomObject]@{
        Status        = $status
        Code          = $code
        Message       = $message
        Target        = $target
        CorrelationId = $correlationId
        Summary       = $summary
    }
}