Is there a way to convert form-data to x-www-form-urlencoded format, while POSTing an api call from Powershell?

185 Views Asked by At

I am writing powershell script in VSCode.

I keep running into the stated issues, when trying to convert the form-data to x-www-form-urlencoded format.

I have a form data as below:

$formData = @{
    'grant_type' = 'password'
    'scope' = 'xyz'
    'client_id' = 'xyzAPIClient'
    'client_secret' = 'xyzabcdef'
    'username' = '[email protected]'
    'password' = 'password1'
}

I tried to convert it to x-www-form-urlencoded format, in order to use it as below (to post a request to the server):

$formUrlEncoded = $formData | ForEach-Object {"$($_.Key)=$($_.Value)"} -join '&'
$response = Invoke-RestMethod -Uri $apiEndPoint -Method Post -Body $formUrlEncoded -Headers $headers

I run into the below issue

ForEach-Object : Cannot bind parameter 'RemainingScripts'. Cannot convert the "-join" value of type "System.String" to type 
"System.Management.Automation.ScriptBlock".
At C:\Automation\automation-scripts\automation-getToken.ps1:15 char:31
+ ... oded = $formData | ForEach-Object {"$($_.Key)=$($_.Value)"} -join '&'
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [ForEach-Object], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.ForEachObjectCommand

I am new to powershell and I couldn't understand what is causing this issue. Could somebody help

2

There are 2 best solutions below

1
Santiago Squarzon On BEST ANSWER

To explain the error you're getting, this error happens because PowerShell is assuming that -join is being passed as argument of ForEach-Object this is why you need to wrap your expression with the grouping operator ( ), basically to, as explained in the documentation, "let output from a command participate in an expression". In addition, hashtables are not enumerable by default in PowerShell, you must use the .GetEnumerator() method:

$formData = @{
    'grant_type'    = 'password'
    'scope'         = 'xyz'
    'client_id'     = 'xyzAPIClient'
    'client_secret' = 'xyzabcdef'
    'username'      = '[email protected]'
    'password'      = 'password1'
}
$formUrlEncoded = ($formData.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '&'
$formUrlEncoded

# Outputs:
# [email protected]&client_id=xyzAPIClient&password=password1&scope=xyz&grant_type=password&client_secret=xyzabcdef

Taking a step back, the -Body parameter from Invoke-RestMethod takes IDIctionary as input and already knows how to deal with instances implementing the interface, so it is very likely that you don't need to join the key / value pairs to form the url-encoded-string.

In summary, it is likely that this should work (as it becomes evident you're trying to get a token from the Graph API, see the examples from this answer and this answer):

$formData = @{
    'grant_type'    = 'password'
    'scope'         = 'xyz'
    'client_id'     = 'xyzAPIClient'
    'client_secret' = 'xyzabcdef'
    'username'      = '[email protected]'
    'password'      = 'password1'
}

$response = Invoke-RestMethod -Uri $apiEndPoint -Method Post -Body $formData -Headers $headers

Side note, in case you do need to form the url-encoded-string, using the HttpUtility APIs can be useful here:

Add-Type -AssemblyName System.Web

$query = [System.Web.HttpUtility]::ParseQueryString('')
$formData.GetEnumerator() | ForEach-Object { $query[$_.Key] = $_.Value }

# if you need URL encoded:
$query.ToString()
# username=user1%40xyz.com&client_id=xyzAPIClient&password=password1&scope=xyz&grant_type=password&client_secret=xyzabcdef

# if you need URL decoded:
[System.Web.HttpUtility]::UrlDecode($query.ToString())
# [email protected]&client_id=xyzAPIClient&password=password1&scope=xyz&grant_type=password&client_secret=xyzabcdef
0
sirtao On

You are using a Hashtable, you cannot access it looping that way

Specifically: it's a "single object", though it's made of multiple values. It's like piping a string: it's not going to iterate through each character.

This should work
$formUrlEncoded = ($formdata.keys | ForEach-Object { "$_=$($formdata.$_)" }) -join '&'