Search code examples
powershellform-datax-www-form-urlencoded

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


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


Solution

  • 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