Private/Common/Invoke-MgcHttpRequest.ps1

function Invoke-MgcHttpRequest {
    <#
    .SYNOPSIS
        Issues an HTTP request and returns a uniform response regardless of PS version.

    .DESCRIPTION
        PowerShell 7 added 'Invoke-WebRequest -SkipHttpErrorCheck' which suppresses
        the exception on 4xx/5xx and returns the response object. Windows PowerShell 5.1
        doesn't have that switch - it always throws on HTTP errors, and the response is
        available only via $_.Exception.Response (with a different shape).

        This helper unifies both behaviors. Returns a PSCustomObject with:
          - StatusCode: [int] HTTP status code
          - Headers: hashtable of response headers
          - Content: string (decoded UTF-8) of the response body
          - ContentBytes: [byte[]] raw response body (only when -ReturnBytes), for
                          binary-correct downloads across PS 5.1 / 7.x.

    .PARAMETER Parameters
        Hashtable of parameters to pass to Invoke-WebRequest (Uri, Method, Headers, Body, etc.).
        Do NOT set SkipHttpErrorCheck or ErrorAction - this helper manages those.

    .PARAMETER ReturnBytes
        Capture the raw response body as a [byte[]] in ContentBytes (read from
        RawContentStream, which is binary-safe on every PowerShell version, unlike
        the .Content property which is a string on PS 7.4+). On success Content is
        left $null to avoid materializing large downloads as a string; on an error
        status the bytes are also decoded into Content so the caller can surface the
        Graph error body.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Parameters,
        [switch]$ReturnBytes
    )

    $useSkipFlag = $PSVersionTable.PSVersion.Major -ge 7

    # PS 5.1's Invoke-WebRequest defaults to IE-based parsing which is slow + brittle.
    if (-not $useSkipFlag) {
        $Parameters['UseBasicParsing'] = $true
    }

    $bodyToString = {
        param($c)
        if ($null -eq $c) { return $null }
        if ($c -is [byte[]]) {
            if ($c.Length -eq 0) { return $null }
            return [System.Text.Encoding]::UTF8.GetString($c)
        }
        return [string]$c
    }

    $headersToHashtable = {
        param($h)
        $out = @{}
        if ($null -eq $h) { return $out }
        # PS 7 headers may be IDictionary-like or WebHeaderCollection
        if ($h -is [System.Collections.IDictionary]) {
            foreach ($k in $h.Keys) { $out[$k] = $h[$k] }
        } elseif ($h.AllKeys) {
            foreach ($k in $h.AllKeys) { $out[$k] = $h[$k] }
        }
        return $out
    }

    # RawContentStream is a MemoryStream on both PS 5.1 and 7.x and holds the
    # undecoded bytes - the only version-safe source for binary downloads.
    $streamToBytes = {
        param($resp)
        $stream = $resp.RawContentStream
        if (-not $stream) { return $null }
        try {
            $ms = New-Object System.IO.MemoryStream
            if ($stream.CanSeek) { $stream.Position = 0 }
            $stream.CopyTo($ms)
            return $ms.ToArray()
        } finally {
            if ($ms) { $ms.Dispose() }
        }
    }

    $buildResult = {
        param($resp)
        $status = [int]$resp.StatusCode
        if ($ReturnBytes) {
            $bytes   = & $streamToBytes $resp
            # Decode to string only on error so the caller can surface the Graph
            # error body; success keeps Content $null to avoid huge strings.
            $content = if ($status -ge 400 -and $bytes) { [System.Text.Encoding]::UTF8.GetString($bytes) } else { $null }
            return [pscustomobject]@{
                StatusCode   = $status
                Headers      = (& $headersToHashtable $resp.Headers)
                Content      = $content
                ContentBytes = $bytes
            }
        }
        return [pscustomobject]@{
            StatusCode   = $status
            Headers      = (& $headersToHashtable $resp.Headers)
            Content      = (& $bodyToString $resp.Content)
            ContentBytes = $null
        }
    }

    if ($useSkipFlag) {
        $Parameters['SkipHttpErrorCheck'] = $true
        $Parameters['ErrorAction']        = 'Stop'
        $resp = Invoke-WebRequest @Parameters
        return (& $buildResult $resp)
    }

    # Windows PowerShell 5.1 path: throws on 4xx/5xx, catch and extract.
    try {
        $Parameters['ErrorAction'] = 'Stop'
        $resp = Invoke-WebRequest @Parameters
        return (& $buildResult $resp)
    } catch [System.Net.WebException] {
        $errResp = $_.Exception.Response
        if (-not $errResp) { throw }

        $statusCode = [int]$errResp.StatusCode

        $body   = $null
        $stream = $null
        $reader = $null
        try {
            $stream = $errResp.GetResponseStream()
            if ($stream) {
                $reader = New-Object System.IO.StreamReader($stream)
                $body = $reader.ReadToEnd()
            }
        } catch {
        } finally {
            if ($reader) { $reader.Dispose() }   # also disposes the stream
            elseif ($stream) { $stream.Dispose() }
        }

        $headers = & $headersToHashtable $errResp.Headers

        return [pscustomobject]@{
            StatusCode   = $statusCode
            Headers      = $headers
            Content      = $body
            ContentBytes = $null
        }
    }
}