22

I'm trying to send a file via Invoke-RestMethod in a similar context as curl with the -F switch.

Curl Example

curl -F FileName=@"/path-to-file.name" "https://uri-to-post"

In powershell, I've tried something like this:

$uri = "https://uri-to-post"
$contentType = "multipart/form-data"
$body = @{
    "FileName" = Get-Content($filePath) -Raw
}

Invoke-WebRequest -Uri $uri -Method Post -ContentType $contentType -Body $body
}

If I check fiddler I see that the body contains the raw binary data, but I get a 200 response back showing no payload has been sent.

I've also tried to use the -InFile parameter with no luck.

I've seen a number of examples using a .net class, but was trying to keep this simple with the newer Powershell 3 commands.

Does anyone have any guidance or experience making this work?

3
  • 1
    Did u tried this -stackoverflow.com/questions/12251965/… Commented Mar 19, 2014 at 4:17
  • 1
    Multipart messages are not supported. You can set the content type to be anything, but setting it to multipart/form-data does not cause the message to be formatted as multipart. Commented Mar 19, 2014 at 13:26
  • 1
    Multipart/form-data does work when adding as content type. The problem I had here is the API needed some parameters to accept raw content, then I was able to use -InFile \path\to\file and upload using Invoke-RestMethod Commented Mar 21, 2014 at 15:54

5 Answers 5

24

The accepted answer won't do a multipart/form-data request, but rather a application/x-www-form-urlencoded request forcing the Content-Type header to a value that the body does not contain.

One way to send a multipart/form-data formatted request with PowerShell is:

$ErrorActionPreference = 'Stop'

$fieldName = 'file'
$filePath = 'C:\Temp\test.pdf'
$url = 'http://posttestserver.com/post.php'

Try {
    Add-Type -AssemblyName 'System.Net.Http'

    $client = New-Object System.Net.Http.HttpClient
    $content = New-Object System.Net.Http.MultipartFormDataContent
    $fileStream = [System.IO.File]::OpenRead($filePath)
    $fileName = [System.IO.Path]::GetFileName($filePath)
    $fileContent = New-Object System.Net.Http.StreamContent($fileStream)
    $content.Add($fileContent, $fieldName, $fileName)

    $result = $client.PostAsync($url, $content).Result
    $result.EnsureSuccessStatusCode()
}
Catch {
    Write-Error $_
    exit 1
}
Finally {
    if ($client -ne $null) { $client.Dispose() }
    if ($content -ne $null) { $content.Dispose() }
    if ($fileStream -ne $null) { $fileStream.Dispose() }
    if ($fileContent -ne $null) { $fileContent.Dispose() }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Is there a way to supply cookies or credentials with this method?
If you are going to make multiple HTTP multiple times (i.e. run this in a loop), you shouldn't dispose the HttpClient instance: aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong
15

The problem here was what the API required some additional parameters. Initial request required some parameters to accept raw content and specify filename/size. After setting that and getting back proper link to submit, I was able to use:

Invoke-RestMethod -Uri $uri -Method Post -InFile $filePath -ContentType "multipart/form-data"

4 Comments

What were the additional parameters you had to add? I'm having the exact same problem using the ShareFile api...
Really depends, did you read api.sharefile.com/rest/docs/resource.aspx?name=Items You can try something like this: sf/v3/Items($parentFolderId)/Upload?method=standard&raw=true&fileName=$fileName&fileSize=$fileSize Obviously replacing those variables with your data
The question I have is was this specific to the API you were trying to use or would this be a general requirement for file uploads using multipart posts? JIRA's API documentation only lists an example with curl (and mentions some header requirements), with no other post parameters.
This doesn't create the multipart body format required, the body will not have the boundary=------------------------abcdefg1234 parts separating the content. See stackoverflow.com/q/25075010/516748
7

I found this post and changed it a bit

$fileName = "..."
$uri = "..."

$currentPath = Convert-Path .
$filePath="$currentPath\$fileName"

$fileBin = [System.IO.File]::ReadAlltext($filePath)
$boundary = [System.Guid]::NewGuid().ToString()
$LF = "`r`n"
$bodyLines = (
    "--$boundary",
    "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
    "Content-Type: application/octet-stream$LF",
    $fileBin,
    "--$boundary--$LF"
) -join $LF

Invoke-RestMethod -Uri $uri -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines

Comments

2

Update Powershell 7+

Invoke-RestMethod and Invoke-WebRequest now both accept a -Form parameter that takes a hashtable like this:

$Uri = 'https://api.contoso.com/v2/profile'
$Form = @{
    firstName  = 'John'
    lastName   = 'Doe'
    email      = '[email protected]'
    avatar     = Get-Item -Path 'c:\Pictures\jdoe.png'
    birthday   = '1980-10-15'
    hobbies    = 'Hiking','Fishing','Jogging'
}
$Result = Invoke-WebRequest -Uri $Uri -Method Post -Form $Form

You can check your current version with $PSVersionTable

See Also

1 Comment

Thank you so much! Finally managed to solve a problem I've had for a while uploading a CSV to Canvas LMS REST API thanks to you. Much appreciated.
0

For anyone wondering (like Jelphy) whether David's answer can be used with cookies/credentials, the answer is yes.

First set the session with Invoke-WebRequest:

Invoke-WebRequest -Uri "$LoginUri" -Method Get -SessionVariable 'Session'

Then POST to the Login URL, which stores the authentication cookie in $Session:

$Response = Invoke-WebRequest -Uri "$Uri" -Method Post -Body $Body -WebSession $Session

The steps above are the standard way to deal with session in Powershell. But here is the important part. Before creating the HttpClient, create an HttpClientHandler and set it's CookieContainer property with the cookies from the session:

$ClientMessageHandler = New-Object System.Net.Http.HttpClientHandler
$ClientMessageHandler.CookieContainer = $Session.Cookies

Then pass this object to the HttpClient constructor

$Client = [System.Net.Http.HttpClient]::new($ClientMessageHandler)

Voila, you now have an HttpClient with session cookies set automatically via Invoke-WebRequest. The rest of David's example should work (copied here for completeness):

$MultipartFormData = New-Object System.Net.Http.MultipartFormDataContent
$FileStream = [System.IO.File]::OpenRead($FilePath)
$FileName = [System.IO.Path]::GetFileName($FilePath)
$FileContent = New-Object System.Net.Http.StreamContent($FileStream)
$MultipartFormData.Add($FileContent, $FieldName, $FileName)

$Result = $Client.PostAsync($url, $content).Result
$Result.EnsureSuccessStatusCode()

I had many files to upload with each request, so I factored out this last bit into a lambda function:

function Add-FormFile {
    param ([string]$Path, [string]$Name)

    if ($Path -ne "")
    {    
        $FileStream = [System.IO.File]::OpenRead($Path)
        $FileName = [System.IO.Path]::GetFileName($Path)
        $FileContent = [System.Net.Http.StreamContent]::new($FileStream)
        $MultipartFormData.Add($FileContent, $Name, $FileName)
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.