Modules/businessdev.ALbuild.Marketplace/Private/Invoke-BcIngestionRestMethod.ps1
|
function Invoke-BcIngestionRestMethod { <# .SYNOPSIS Sends a body-bearing write (PUT/POST) to the Partner Center Product Ingestion API the way the proven BcContainerHelper implementation does, and surfaces the real error on failure. .DESCRIPTION The submission flow originally called Invoke-RestMethod inline with a raw JSON *string* body and Content-Type 'application/json'. Microsoft's proven AppSource ingestion client (BcContainerHelper's Invoke-IngestionApiRestMethod) instead sends the body as **UTF-8 bytes** with Content-Type 'application/json; charset=utf-8', always carries the resource ETag in the If-Match header, and retries transient 5xx/429 responses. A raw-string body of a real product resource - whose properties include localised / free-text fields - can be mis-encoded by the host into malformed JSON that the ingestion API rejects with a bare "400 Bad Request". This helper aligns the mechanics to the proven client and, on failure, appends the Partner Center response body (via Get-BcMarketplaceErrorDetail) so the actual reason is logged instead of just the status code. .PARAMETER Uri The full ingestion API URI. .PARAMETER Method The HTTP method (Put/Post/Get/Delete). .PARAMETER Headers Base request headers (Authorization etc.). Cloned; Content-Type/If-Match are set here. .PARAMETER Body The request body: a hashtable/PSObject (serialised to JSON here) or a pre-serialised JSON string. Omit for methods without a body. .PARAMETER ETag The resource's '@odata.etag'; sent as the If-Match header when present. .PARAMETER Operation A short human description used in the thrown error and retry logs (e.g. 'update the product version'). .OUTPUTS The deserialised API response (whatever Invoke-RestMethod returns). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Uri, [Parameter(Mandatory)] [ValidateSet('Get', 'Post', 'Put', 'Delete')] [string] $Method, [Parameter(Mandatory)] [hashtable] $Headers, [object] $Body, [string] $ETag, [string] $Operation = 'the ingestion request' ) $h = $Headers.Clone() $params = @{ Uri = $Uri; Method = $Method; Headers = $h; ErrorAction = 'Stop' } if ($null -ne $Body) { # UTF-8 bytes + explicit charset: exactly what the proven BcContainerHelper client sends. Passing a # raw string here can be mis-encoded and produce JSON the ingestion API rejects with 400. $h['Content-Type'] = 'application/json; charset=utf-8' $json = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 20 } $params['Body'] = [System.Text.Encoding]::UTF8.GetBytes($json) } if ($ETag) { $h['If-Match'] = $ETag } $attempt = 0 while ($true) { $attempt++ try { return Invoke-RestMethod @params } catch { # StrictMode-safe: reference `.Response` via PSObject so an exception type WITHOUT that member # (anything other than WebException/HttpResponseException) reads as $null instead of throwing # PropertyNotFoundException. Fall back to parsing the status out of the message ("(400)"). $status = 0 $respProp = $_.Exception.PSObject.Properties['Response'] if ($respProp -and $respProp.Value) { try { $status = [int] $respProp.Value.StatusCode } catch { $status = 0 } } if (-not $status -and "$($_.Exception.Message)" -match '\((\d{3})\)') { $status = [int] $Matches[1] } # Transient server-side failures: retry a few times with backoff (mirrors the proven client). if ($status -in @(429, 500, 502, 503, 504) -and $attempt -le 5) { Write-ALbuildLog -Level Warning "HTTP $status while trying to $Operation; retry $attempt/5 in $($attempt * 5)s..." Start-Sleep -Seconds ($attempt * 5) continue } # Surface the Partner Center response body - Invoke-RestMethod only exposes the status line. $detail = Get-BcMarketplaceErrorDetail -ErrorRecord $_ throw "Failed to $Operation ($($_.Exception.Message))$(if ($detail) { " | Partner Center response: $detail" })." } } } |