Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Posting `multipart/form-data` with Flurl

I am in need to post the following request:

POST http://target-host.com/some/endpoint HTTP/1.1
Content-Type: multipart/form-data; boundary="2e3956ac-de47-4cad-90df-05199a7c1f53"
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 6971
Host: target-host.com

--2e3956ac-de47-4cad-90df-05199a7c1f53
Content-Disposition: form-data; name="some-label"

value
--2e3956ac-de47-4cad-90df-05199a7c1f53
Content-Disposition: form-data; name="file"; filename="my-filename.txt"

<file contents>
--2e3956ac-de47-4cad-90df-05199a7c1f53--

I can do this really easily with Python requests library as follows:

import requests

with open("some_file", "rb") as f:
    byte_string = f.read()

requests.post(
    "http://target-host.com/some/endpoint",
    data={"some-label": "value"},
    files={"file": ("my-filename.txt", byte_string)})

Is there any way to do the same with the Flurl.Http library?

My problem with the documented way of doing it is that it will insert the Content-Type header for each key-value pair and it will insert the filename*=utf-8'' header for the file data. The server I am trying to post the request to, however, does not support this. Also note the double quotes around the name and filename values in the headers.

EDIT: Below is the code I used to make the post request with Flurl.Http:

using System.IO;
using Flurl;
using Flurl.Http;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var fs = File.OpenRead("some_file");

            var response = "http://target-host.com"
                .AppendPathSegment("some/endpoint")
                .PostMultipartAsync(mp => mp
                    .AddString("some-label", "value")
                    .AddFile("file", fs, "my-filename.txt")
                ).Result;
        }
    }
}
like image 385
aignas Avatar asked Aug 16 '16 05:08

aignas


1 Answers

According the spec (dated June 2011), sending both filename and filename* is recommended for maximum compatibility:

Many user agent implementations predating this specification do not understand the "filename*" parameter. Therefore, when both "filename" and "filename*" are present in a single header field value, recipients SHOULD pick "filename*" and ignore "filename". This way, senders can avoid special-casing specific user agents by sending both the more expressive "filename*" parameter, and the "filename" parameter as fallback for legacy recipients.

If filename* is actually causing the call to fail, there's a real problem with the server adhering to the HTTP spec. Also, enclosing name and filename in quotes is very non-standard.

That said, Flurl's shortcuts cover the 90% cases, but you can always use the underlying HttpClient APIs to cover unusual cases like this one. In this case I think you need to build up the content manually so you can deal with those Content-Disposition headers:

var mpc = new MultipartContent();
var sc = new StringContent("value");
sc.Headers.Add("Content-Disposition", "form-data; name=\"some-label\"");
mpc.Add(sc);
var fc = new StreamContent(fs);
fc.Headers.Add("Content-Disposition", "form-data; name=\"file\"; filename=\"my-filename.txt\"");
mpc.Add(fc);

Then you can use it with Flurl like this:

var response = await "http://target-host.com"....PostAsync(mpc);
like image 58
Todd Menier Avatar answered Nov 03 '22 08:11

Todd Menier