Search code examples
jsonpowershellmethodspscustomobjectinvoke-restmethod

Powershell Invoke-RestMethod : body content is not interpreted correctly when send PUT Request


I have built a PUT Request using Invoke-RestMethod in a powershell script.

This request looks like :

# Main global var
$securePassword = ConvertTo-SecureString -String $pat -AsPlainText
$credential = [PSCredential]::new($username, $securePassword)
$organization = "myorganization"
$project = "myproject"
$username = "username"
$pat = "myRandomPatFromAzureDevOps"
# AZDO build env var
$buildNumber = "test1.0"
# AZDO Wiki global vars
$page = "Release-Notes-$buildNumber"

$Body = [PsCustomObject]@{ 
    "content" = "$results"
} | ConvertTo-Json 

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

Invoke-RestMethod -Uri "https://dev.azure.com/$organization/$project/_apis/wiki/wikis/MyWiki.wiki/pages?path=$page&api-version=7.0" -Method 'PUT' -Authentication Basic -Credential $credential -Headers $headers -Body $Body

As we can see on the azure devops rest api in order to add content in the wiki repository here, in order to update the wiki I have to give in the Request Body a parameter named content into string format.

This the content of my var $results pass to the content parameter :

|Id|AreaPath|IterationPath|WorkItemType|State|Title|CloseDate|
|---------|---------------|--------------------|-------------------|------------|------------|--------------------------------|
|8888|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Bug|Closed|Parts Unlimited Sprint log security password|03-03-2023 13:31:37|
|9999|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Task|Closed|Parts Unlimited SprintAutomatisation APIs |03-01-2023 08:32:31|

As you can see, I am trying to pass a MarkDown table as a string. For format the result like this I have found and used a function named ConvertTo-MarkDownTable :

function ConvertTo-MarkDownTable {
    [CmdletBinding()] param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] 
        $InputObject
    )
    Begin { 
        $headersDone = $false
        $pattern = '(?<!\\)\|'  # escape every '|' unless already escaped
    }
    Process {
        if (!$headersDone) {
            $headersDone = $true
            # output the header line and below that a dashed line
            # -replace '(?<!\\)\|', '\|' escapes every '|' unless already escaped
            "`n|{0}|" -f (($_.PSObject.Properties.Name -replace $pattern, '\|') -join '|')
            "`n|{0}|" -f (($_.PSObject.Properties.Name -replace '.', '-') -join '|')
        }
        "`n|{0}|" -f (($_.PsObject.Properties.Value -replace $pattern, '\|') -join '|')
    }
}

The initial data in $results are values that I recovered from a GET Method of Azure DevOps Rest API. When I see the type of my $results, I have this :

# $results.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

Now when I run the Invoke-RestMethod, noting is sent in the body content parameter :

Invoke-RestMethod -Uri "https://dev.azure.com/$organization/$project/_apis/wiki/wikis/MyWiki.wiki/pages?path=$page&api-version=7.0" -Method 'PUT' -Authentication Basic -Credential $credential -Headers $headers -Body $Body

Output :
path        : /path-to-push-content
order       : 1
gitItemPath : /path%2Dto%2Dcontent.md
subPages    : {}
url         : https://dev.azure.com/myorganization/id-of-event-etag/_apis/wiki/wikis/fid-of-wiki-request/pages
              /%2Fpath-to-push-content
remoteUrl   : https://dev.azure.com/myorganization/id-of-event-etag/_wiki/wikis/fid-of-wiki-request?pagePath=%
              2Fpath-to-push-content
id          : randomId
content     :

But I have tested this request using POSTMAN. And everything is working well. This is the request : enter image description here

As you can see, the results is parsed and interpreted correctly.

And this is the upload on azure devops wiki : enter image description here

Why my PUT method request does not sent content in my powershell script as POSTMAN do ?


Solution

  • Since you're passing a media type of application/json:

    • You must pass a JSON string to Invoke-WebRequest's -Body parameter

      • Since $results is an array of lines, you must manually join its elements with newlines to form a single, multi-line string: $results -join "`n". If you rely on implicit stringification inside an expandable string ("$results"), the elements are joined with spaces instead.
    • PowerShell will not automatically convert a [pscustomobject] or hashtable to JSON for you.

    Therefore, construct your $Body variable as follows, using ConvertTo-Json:

    $Body = @{ 
      "content" = $results -join "`n"
    } | ConvertTo-Json 
    
    • Note how passing a hashtable rather than a [pscustomobject] is sufficient.

    • A general caveat (it doesn't apply here): More deeply nested objects / hashtables may get truncated, unless you pass a sufficiently high -Depth value to ConvertTo-Json - see this post.


    Here's a self-contained example that demonstrates that a JSON string passed to -Body is posted correctly:

    • https://postman-echo.com/put echoes the input JSON in the data property of the JSON response it returns.
    • Using Invoke-RestMethod instead of Invoke-WebRequest automatically parses that JSON response into a [pscustomobject] graph, which makes it easier to visualize the successful round trip.
    $results = @'
    |Id|AreaPath|IterationPath|WorkItemType|State|Title|CloseDate|
    |---------|---------------|--------------------|-------------------|------------|------------|--------------------------------|
    |8888|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Bug|Closed|Parts Unlimited Sprint log security password|03-03-2023 13:31:37|
    |9999|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Task|Closed|Parts Unlimited SprintAutomatisation APIs |03-01-2023 08:32:31|
    '@
    
    $body = @{ 
      "content" = $results
    } | ConvertTo-Json 
    
    (
      Invoke-RestMethod https://postman-echo.com/put `
        -Method PUT `
        -ContentType application/json `
        -Body $body
    ).data |
      Format-List
    

    Output, which shows that the $results value was properly submitted:

    content : |Id|AreaPath|IterationPath|WorkItemType|State|Title|CloseDate|
              |---------|---------------|--------------------|-------------------|------------|------------|--------------------------------|
              |8888|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Bug|Closed|Parts Unlimited Sprint log security password|03-03-2023 13:31:37|
              |9999|Parts Unlimited Team|Parts Unlimited Sprint 2023-03|Task|Closed|Parts Unlimited SprintAutomatisation APIs |03-01-2023 08:32:31|