A common way 3rd party APIs handle file uploads is via an HTTP multi-part form. The API documentation might point to some dry RFC documentation like this
The short of it is you must invoke an HTTP POST request with the body formatted, to use the example in the linked document, like this:
Content-type: multipart/form-data, boundary=AaB03x --AaB03x content-disposition: form-data; name="field1" Joe Blow --AaB03x content-disposition: form-data; name="pics"; filename="file1.txt" Content-Type: text/plain ... contents of file1.txt ... --AaB03x--
In the Power Automate & Logic Apps HTTP action you can write this plain text directly into the body, and it works OK as long as the file content is plain text.
I’m using Bamboo HR as an example below. In this screen shot, the upload works fine, if you want to create a file called readme.txt containing “This is a sample text file.”
But what about binary content? What if I want to upload an image or PDF?
Your first thought might be put some dynamic content where “This is a sample text file.” is:
Does it work? Well sort of. The HTTP action succeeds and returns a 201 status.
However, the file in the destination is corrupt. It’s there, and the file size is sort of right, but the file is corrupt.
If you open both files in Notepad you can see something has happened to to the content. The one on the left is the original file and the right is the one downloaded from the upstream service we just posted the file to:
If you try and convert to base64 you get similar results – a file, but a corrupt one with the contents markedly different from the original when you open in a text editor.
So how do you solve this? There’s isn’t really a great deal of documentation around to help. The nearest you get is the Logic Apps HTTP connector documentation (while this is the Logic Apps documentation, Power Automate is built on Logic Apps so it applies equally there too).
Although this small piece of documentation is highly valuable in solving the problem, it misses a couple of key requirements: Attributes and the schema of the file content.
In my example above the file has several attributes: a category, which in my case is represented by a numeric string, 18, a file name (this is separate from the actual filename within the header of the file section of the multi-part form) and also a boolean, shared, which is represented by a text string “yes”. Also omitted from the Microsoft material is the schema of the body property of the file. They simply put trigger() in their example as the body, with no explanation as to what that actually is.
So how do you construct the JSON body of the HTTP action to include those attributes?
I’ll save you the hassle of finding out by explaining below, but I will say how I found out:
You can create a new Flow that’s triggered on “when an HTTP request is received”. Change the method to POST and then add a compose action. Put the “Body” dynamic content into the compose and save. Copy the URL for the trigger and go back to your original Flow.
Next, change the URL in the HTTP POST action to the one in your clipboard and remove any authentication parameters, then run it.
Your new flow will trigger and in the compose action you should see the multi-part form data received in the POST request. You can now start playing around with the JSON in the HTTP body until you get something that resembles your API documentation.
So, this is the result of that process:
It’s actually pretty simple when you look at it. The body object has two attributes; $content-type and $multipart.
The HTTP action interprets the $content-type property and puts the right data into the content-type header of the request. There’s actually no need for you to specify any headers at all in your HTTP action, unless for authentication purposes.
The $multipart attribute is an array, and each object in the array is a separate part of the form, so for every point in the plain text you’d put a form boundary, that’s another object in the array.
Each object in the array has a headers and a body attribute. :
Content-Disposition: form-data; name=”category”
“Content-Disposition”: “form-data; name=\”category\”“
In the case of the actual file, there’s a quirk. You would expect, based on the following, that there would be two headers; Content-Disposition and Content-Type:
But that’s actually not so. The JSON must be constructed like as follows. The content-type is part of the body:
“Content-Disposition”: “form-data; name=\”file\”; filename=\”test.jpg\””
Note the need to use a backslash to break out of the inverted commas within the Content-Disposition header value.
Depending on where you’re getting your content from, you might already have the whole body object in the right format.
For example the output of SharePoint and OneDrive Get file content, and OneDrive’s Convert File actions contain a body with the $content-type and $content attributes, with the $content in Base64 already:
So, if you’re using an action like that earlier in your flow, you could just insert the “File content” dynamic content directly into the body like this:
Your mileage may vary: If you’re getting the file content from a connector that outputs binary, or indeed base64 but not in the same format as the Get file content actions, then you will need include those properties in your HTTP body and possibly convert the dynamic content into base64 first using the base64() function in the expression builder.
You would also need to get your content type from somewhere too, if the upstream API you’re posting to cares about it.
To finish, I need to give some credit to SamPo on the Microsoft Power Automate forums, who figured all this out long before I did and provided great help in my time of need.
If you feel like this has saved you time and frustrations, feel free to leave a small donation via Paypal: