Private/Invoke-AzTableRestMethod.ps1

function Invoke-AzTableRestMethod {
    <#
    .SYNOPSIS
        Sends an authenticated HTTP request to the Azure Table Storage REST API.

    .DESCRIPTION
        Builds the full request URL (appending the SAS token when applicable),
        obtains the correct authorization headers, and dispatches the request.
        Returns a PSCustomObject with Content (parsed JSON) and Headers
        (response headers) so that callers can inspect continuation tokens.

    .OUTPUTS
        [PSCustomObject] with properties:
          - Content : Parsed JSON body, or $null for empty responses
          - Headers : Response header collection
          - StatusCode : HTTP status code (int)
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory)]
        [PSCustomObject]$Context,

        [Parameter(Mandatory)]
        [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')]
        [string]$Method,

        # Path after the base endpoint, e.g. "Tables", "mytable()", "Tables('mytable')"
        [Parameter(Mandatory)]
        [string]$Resource,

        # Optional OData query string (without leading '?'), e.g. "$filter=PartitionKey eq 'pk'"
        [string]$QueryString = '',

        # Optional request body (will be serialized to JSON)
        [object]$Body = $null,

        # Optional ETag for conditional operations (If-Match header)
        [string]$ETag = $null
    )

    $contentType = ''
    $bodyBytes   = $null

    if ($null -ne $Body) {
        $contentType = 'application/json'
        $bodyJson    = $Body | ConvertTo-Json -Depth 10 -Compress
        $bodyBytes   = [System.Text.Encoding]::UTF8.GetBytes($bodyJson)
    }

    # Build authorization headers
    $authHeaders = Get-AzTableAuthorizationHeader `
        -Context     $Context `
        -Method      $Method `
        -Resource    $Resource `
        -ContentType $contentType

    $requestHeaders = @{
        'DataServiceVersion'    = '3.0;NetFx'
        'MaxDataServiceVersion' = '3.0;NetFx'
        'Accept'                = 'application/json;odata=nometadata'
    }

    foreach ($key in $authHeaders.Keys) {
        $requestHeaders[$key] = $authHeaders[$key]
    }

    if ($ETag) {
        $requestHeaders['If-Match'] = $ETag
    }

    # Build URL - SAS token always goes first so OData params are appended afterward
    $url = "$($Context.Endpoint)/$Resource"

    if ($Context.AuthType -eq 'SasToken') {
        $url += $Context.SasToken      # already starts with '?'
        if ($QueryString) {
            $url += "&$QueryString"
        }
    } elseif ($QueryString) {
        $url += "?$QueryString"
    }

    $requestParams = @{
        Uri             = $url
        Method          = $Method
        Headers         = $requestHeaders
        UseBasicParsing = $true
        ErrorAction     = 'Stop'
    }

    if ($null -ne $bodyBytes) {
        $requestParams['Body']        = $bodyBytes
        $requestParams['ContentType'] = 'application/json'
    }

    try {
        $response = Invoke-WebRequest @requestParams

        $parsedContent = $null
        $rawContent    = $response.Content

        if ($rawContent -is [byte[]]) {
            $rawContent = [System.Text.Encoding]::UTF8.GetString($rawContent)
        }

        if (-not [string]::IsNullOrWhiteSpace($rawContent)) {
            $parsedContent = $rawContent | ConvertFrom-Json
        }

        return [PSCustomObject]@{
            Content    = $parsedContent
            Headers    = $response.Headers
            StatusCode = [int]$response.StatusCode
        }
    } catch {
        $statusCode    = $null
        $errorMessage  = $_.Exception.Message

        if ($_.Exception.Response) {
            $statusCode = [int]$_.Exception.Response.StatusCode
        }

        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            $errorBody = $_.ErrorDetails.Message
            try {
                $errorObj = $errorBody | ConvertFrom-Json
                if ($errorObj.'odata.error'.message.value) {
                    $errorMessage = $errorObj.'odata.error'.message.value
                }
            } catch {
                $errorMessage = $errorBody
            }
        }

        throw [System.Exception]::new(
            "Azure Table Storage request failed (HTTP $statusCode): $errorMessage",
            $_.Exception
        )
    }
}