[SalesForce] Post to Chatter With Binary File from Javascript

So I'm trying to post a feed item to Chatter via the Chatter REST API from Javascript.

There relevant documentation shows that the request must be made as a mutlipart/form-data request, so I'm attempting to do that using the FormData Javascript object.

The example request in the docs looks like this:

POST /services/data/v34.0/chatter/feed-elements HTTP/1.1
Authorization: OAuth 00DRR0000000N0g!...
User-Agent: Jakarta Commons-HttpClient/3.0.1
Host: instance_name
Content-Length: 845
Content-Type: multipart/form-data; boundary=a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Accept: application/json
--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Content-Disposition: form-data; name="json"
Content-Type: application/json; charset=UTF-8
{
   "body":{
      "messageSegments":[
         {
            "type":"Text",
            "text":"Please accept this receipt."
         }
      ]
   },
   "capabilities":{
      "content":{
         "description":"Receipt for expenses",
         "title":"receipt.pdf"
      }
   },
   "feedElementType":"FeedItem",
   "subjectId":"005RR000000DmOb"
}

--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq
Content-Disposition: form-data; name="feedElementFileUpload"; filename="receipt.pdf"
Content-Type: application/octet-stream; charset=ISO-8859-1

...contents of receipt.pdf...

--a7V4kRcFA8E79pivMuV2tukQ85cmNKeoEgJgq--

I'm using the following code:

var item = 
{
    "body":
    {
        "messageSegments" : segments
    },
    "feedElementType" : "FeedItem",
    "subjectId" : "me"
};

if(fileData)
{
    item.capabilities =
    {
        "content" :
        {
            "description": "File attachment from Clienteling",
            "title": "Some File"
        }
    };

    var data = new FormData();
    data.append("json", JSON.stringify(item));
    data.append("feedElementFileUpload", fileData);

    var req = new XMLHttpRequest();

    req.addEventListener("load", function(event)
        {
            success(req);
        }, false);
    req.addEventListener("error", fail, false);

    req.open("POST", self.base + "/feed-elements", true);
    req.setRequestHeader("Authorization", self.bearer);
    req.send(data);
}

Which results in the following request body:

-----------------------------18721583984357993421560304724
Content-Disposition: form-data; name="json"

{"body":{"messageSegments":[{"type":"Text","text":"This is a file post."}]},"feedElementType":"FeedItem","subjectId":"me","capabilities":{"content":{"description":"File attachment","title":"Some File"}}}
-----------------------------18721583984357993421560304724
Content-Disposition: form-data; name="feedElementFileUpload"; filename="39191-ki1RNjpg-EdDM.jpg"
Content-Type: image/jpeg

ÿØÿà�JFIF��H�H��ÿÛ�C�       
SNIP
-----------------------------18721583984357993421560304724--

So basically everything looks except for the fact that the first part of the data, i.e. the JSON, doesn't have a Content-Type part to it, and I think that's what's killing me because I'm getting this response text from Salesforce:

<?xml version="1.0" encoding="UTF-8"?><Errors><Error><errorCode>MISSING_ARGUMENT</errorCode><message>Missing required &apos;subjectId&apos; parameter.</message></Error></Errors>

So… does anyone know how to fix this? I can't find anyway of specifying a content type for FormData parts, which makes me think that the only way forward is to ditch that or just use it for the binary part. Life would be much easier if this API would just accept some base64 encoded data!

Update
Based on Daneil's comment I tried things out in Postman. As I suspected, it's the missing Content-Type: application/json line that's causing the issue.

Best Answer

The answer had been staring me in the face all damned day. I'd read this disclaimer in the docs:

Web browsers are typically incapable of making multipart requests when the non-binary parts, such as rich input bodies, have their own Content-Type. To work around this issue, specify a certain Content-Disposition name and Salesforce can read the Content-Type of the rich input part. (You don’t need to specify a Content-Type for the rich input body.)

But I'd still not twigged that I had to call the JSON part of the form request "feedElement" and not "json" as would be done in requests made from places other than a browser. i.e. this:

data.append("json", JSON.stringify(item));

shoudl be this:

data.append("feedElement", JSON.stringify(item));

I need a beer.