POSTing images to pure F# Web API 2.0
Getting the multi-part form data out of your web API requests
Why am I doing this?
I have a REST API that I built with the Pure F# Web API templates. Mostly my REST API was just designed to output and process JSON, but recently I needed to accept images.
The problem
I need to save some images, through my REST API, that are produced on another system. I need that system to POST those images to my API for storage either in the database or on disk.
I saw a lot of examples. The first few I tried were simple enough but not working?!!11??. So I kept googling and tried like a million things, converting the C# examples into F# code. Although that was a good exercise in learning F#, it was also an exercise in futility. As we will see later, it was a PEBKAC problem and the obvious answer was just fine. Don't let this be you.
The Web API
This turned out to be pretty simple. We can use the Web API MultipartFormDataStreamProvider
class in System.Net.Http
.
Represents a T:System.Net.Http.IMultipartStreamProvider suited for use with HTML file uploads for writing file content to a FileStream.
In human words this means a class you can use to grab files out of a request and then save them to disk, or do other streaming with them (like push to SQL Server's Filestream).
First I decided where to store my files. I just used App_Data like this:
let root = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data")
Here I am using the OWIN/self-hosted compatible class. But you might also use HttpContext.Current.Server.MapPath
Next I declare a new MultipartFormDataStreamProvider like this:
let provider = new MultipartFormDataStreamProvider(root)
Now I can do a little async and read the multi-part data in the Request, which will fill in the provider object with the file information and content.
let readStreamAsync =
this.Request.Content.ReadAsMultipartAsync(provider)
|> Async.AwaitIAsyncResult
|> Async.Ignore
}
And now we have our file content. In this example, the provider has written the file to disk with the default file name. So that looks like this:
Yeah I know. What kind of file name is that? Here is the reason why the Web API team decided to give random file names (hint: security purposes).
Putting it all together my POST action looks like this:
[<HttpPost>]
[<Route("somewhere/students/{studentid}/charts/{chartid}")>]
[<Authorize>]
member this.chart_Post(studentid : string, chartid : string) =
if this.Request.Content.IsMimeMultipartContent() = true then
let root = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data")
let provider = new MultipartFormDataStreamProvider(root)
let readStreamAsync =
this.Request.Content.ReadAsMultipartAsync(provider)
|> Async.AwaitIAsyncResult
|> Async.Ignore
NegotiatedContentResult(HttpStatusCode.OK, "Goodbye and thanks for all the files in " + root + provider.GetLocalFileName(this.Request.Content.Headers), this)
else
NegotiatedContentResult(HttpStatusCode.OK, "No files?", this)
And that's it. So what gave me all the headaches?
I was using Postman and crafting the wrong, but almost right, kind of request. Derp.
Testing with Postman
If you are not using Postman you should be!
In Postman I had to craft a particular request. The more obvious settings were using:
- form-data
- selecting a file in the key-value options
But this wasn't working (argh!). This sent me round in circles.
The tricky bit was a couple of headers I thought I would need. I didn't, but the request wasn't recognized as multi-part form data if I included them. A comment on a StackOverflow question pointed me in the right direction. So I removed these headers:
- Content-Type
- Content-Disposition
My request looked like this in cURL:
curl -X POST \
http://api-local/somewhere/students/000000/charts/4fhfhgdhgd \
-H 'cache-control: no-cache' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-H 'postman-token: ed1f5d50-85d2-0d3e-d901-fa0f98fffb7b' \
-F file=undefined
And that worked. Finally.
Improvements and what's next
I would like to be able to check the file type before saving.
I would like to save the BodyPart as a useful name.
I would like to check the size and limit the size of the files posted.
I would like to see about streaming the files into SQL Server FileStream.
Here is a more in-depth blog post about doing the same thing but with moar stuff.
If you have any comments or questions please tweet me, because the comments doohikey hasn't been working so good.
Full Stack .NET Programmer and Ham