Modules/businessdev.ALbuild.OnPrem/Private/Invoke-BcAutomationRequest.ps1

function Invoke-BcAutomationRequest {
    <#
    .SYNOPSIS
        Calls the Business Central automation API and surfaces the real reason when it fails.
 
    .DESCRIPTION
        Invoke-RestMethod reports only the status line - "The remote server returned an error:
        (400) Bad Request." - while the automation API returns a JSON body naming the actual problem
        (a missing dependency, a schema change that needs Force Sync, an app that is not signed, ...).
        Without that body a failed per-tenant publish is undiagnosable from the release log.
 
        This wrapper reads the response body across Windows PowerShell 5.1 (WebException response
        stream) and PowerShell 7+ (body pre-read into ErrorDetails.Message) and appends it to the
        thrown message, together with the operation being performed and the URI. It mirrors what
        Get-BcMarketplaceErrorDetail does for the Partner Center ingestion API.
 
    .PARAMETER Uri
        The full automation API URI.
 
    .PARAMETER Method
        The HTTP method.
 
    .PARAMETER Headers
        Request headers (Authorization, If-Match, Content-Type, ...).
 
    .PARAMETER Body
        Optional request body (JSON string or byte array).
 
    .PARAMETER Operation
        Short human description used in the error message, e.g. "create the extension upload record".
 
    .OUTPUTS
        The deserialised API response.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Uri,
        [Parameter(Mandatory)] [ValidateSet('Get', 'Post', 'Patch', 'Put', 'Delete')] [string] $Method,
        [Parameter(Mandatory)] [hashtable] $Headers,
        [object] $Body,
        [Parameter(Mandatory)] [string] $Operation
    )

    $params = @{ Uri = $Uri; Method = $Method; Headers = $Headers; ErrorAction = 'Stop' }
    if ($null -ne $Body) { $params['Body'] = $Body }

    try { return Invoke-RestMethod @params }
    catch {
        $status = 0
        $responseProperty = $_.Exception.PSObject.Properties['Response']
        if ($responseProperty -and $responseProperty.Value) {
            try { $status = [int] $responseProperty.Value.StatusCode } catch { $status = 0 }
        }
        if (-not $status -and "$($_.Exception.Message)" -match '\((\d{3})\)') { $status = [int] $Matches[1] }

        $detail = $null
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $detail = "$($_.ErrorDetails.Message)" }
        elseif ($responseProperty -and $responseProperty.Value) {
            try {
                $stream = $responseProperty.Value.GetResponseStream()
                if ($stream) {
                    $reader = New-Object System.IO.StreamReader($stream)
                    try {
                        if ($reader.BaseStream.CanSeek) { $reader.BaseStream.Position = 0 }
                        $detail = $reader.ReadToEnd()
                    }
                    finally { $reader.Close() }
                }
            }
            catch { Write-Verbose "Could not read the error response body: $($_.Exception.Message)" }
        }

        $statusText = if ($status) { "HTTP $status" } else { 'the request' }
        throw ("Failed to $Operation ($statusText): $($_.Exception.Message)" +
            $(if ($detail) { "`nBusiness Central response: $($detail.Trim())" } else { "`n(The API returned no response body.)" }) +
            "`nRequest: $Method $Uri")
    }
}