I'm trying to upload a file to an API using cfhttp...
# cfml-general
h
I'm trying to upload a file to an API using cfhttp but not having any luck. The documentation example is:
Copy code
This API request must have its Content-Type set to multipart/form-data.

curl -v -u api_key:X -F 'attachments[]=@/Users/user/Desktop/api2.png' -F 'priority=2' -X PUT '<https://domain.freshservice.com/api/v2/tickets/51>'
but when I try the following,
Copy code
cfhttp(url="#fsURL#",method="PUT",result="fsResult") {
    cfhttpparam(type="header",name="Authorization",value="Basic #fsAuthorization#");
    cfhttpparam(type="header",name="Content-Type",value="multipart/form-data");
    cfhttpparam(type="file",name="attachments[]",file='e:\fsApi\attachments\#childData.ticket_id#\test.jpg');
}
I get the following error:
Copy code
{"message":"Invalid request parameters: invalid %-encoding (����\"#Exif\u0000\u0000II*\u0000\b\u0000\u0000\u0000\u000B\u0000\u000F\u0001\u0002\u0000\u0014\u0000\u0...\u0019\u001A%)","code":"invalid_encoding"}
If I add priority, then I don't get an error, priority does get set to 2, but the attachment never gets added:
Copy code
cfhttp(url="#fsURL#",method="PUT",result="fsResult") {
    cfhttpparam(type="header",name="Authorization",value="Basic #fsAuthorization#");
    cfhttpparam(type="header",name="Content-Type",value="multipart/form-data");
    cfhttpparam(type="formfield",name="priority",value="2");  
cfhttpparam(type="file",name="attachments[]",file='e:\fsApi\attachments\#childData.ticket_id#\test.jpg');
//cfhttpparam(type="file",name="test.jpg",file='e:\fsApi\attachments\#childData.ticket_id#\test.jpg');
}
I tried the commented out line above too and still no error and no attachment.
When I successfully fetched an attachment from the API, I had to use the following "getAsBinary" attribute so it seems I must be missing something from the PUT along the same lines.
Copy code
cfhttp(url="#fsURL#",method="#fsMethod#",result="fsResult",getasbinary="yes") {
      cfhttpparam(name="Content-Type",type="header",value="application/json");
      cfhttpparam(name="Authorization",type="header",value="Basic #fsAuthorization#");
    }
j
Are you using ACF? You could try adding
multipart="yes"
to the cfhttp call. I have had issues mixing formfield and file in the same request with ACF. Maybe a bug?
Now that I recall, I was mixing json data with a file. Not formfield and a file. So using `multipart="yes" may do the trick for you.
fingers crossed 1
If not, I had to work around it by building the body of the request manually like this.
Copy code
var requestBody = [];
var fieldBoundary = "--------------" & getTickCount();
requestBody.append("#NEW_LINE##NEW_LINE#--#fieldBoundary##NEW_LINE#");
requestBody.append("Content-Disposition: form-data; name=""importRequest""#NEW_LINE#");
requestBody.append("Content-Type: application/json#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( serializeJSON( myJSONData ) );


requestBody.append("#NEW_LINE#--#fieldBoundary##NEW_LINE#");
requestBody.append("Content-Disposition: form-data; name=""attachments[]""; filename=""#file.getFormattedName()#.csv""#NEW_LINE#");
requestBody.append("Content-Type: text/csv#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( fileRead('e:\fsApi\attachments\#childData.ticket_id#\test.jpg', "utf-8") );
requestBody.append("#NEW_LINE#--#fieldBoundary#--#NEW_LINE#");

var requestParameters = [
    { type: "header", name: "Content-Type", value: "multipart/form-data; boundary=#fieldBoundary#" },
    { type: "body", value: requestBody.toList( "" ) }
];

cfhttp(
    method=fsMethod,
    charset="utf-8",
    url=fsURL,
    result="httpResponse"
) {
    for ( var httpParameter in httpParameters ) {
        cfhttpparam( attributeCollection = httpParameter  );
    }
}
Instead of
requestBody.append( serializeJSON( myJSONData ) );
You would need to put in the form field data.
h
I tried adding multipart="yes" and the API errors out with a a 500.
I' m using Adobe Coldfusion 2021
q
The CFHTTP tag will overwrite your content-type fields on outgoing calls. Try pointing the cfhttp call to a webhook.site url to see what is actually being sent out.
👍 1
j
you could try this workaround to see if it works for you.
Copy code
```
var NEW_LINE = chr(13) & chr(10);
var requestBody = [];
var fieldBoundary = "--------------" & getTickCount();
requestBody.append("#NEW_LINE##NEW_LINE#--#fieldBoundary##NEW_LINE#");
requestBody.append("Content-Disposition: form-data#NEW_LINE#");
requestBody.append("Content-Type: application/x-www-form-urlencoded#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( "priority=2" );


requestBody.append("#NEW_LINE#--#fieldBoundary##NEW_LINE#");
requestBody.append("Content-Disposition: form-data; name=""attachments[]""; filename=""#file.getFormattedName()#.csv""#NEW_LINE#");
requestBody.append("Content-Type: text/csv#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( fileRead('e:\fsApi\attachments\#childData.ticket_id#\test.jpg', "utf-8") );
requestBody.append("#NEW_LINE#--#fieldBoundary#--#NEW_LINE#");

var requestParameters = [
	{ type: "header", name: "Authorization", value: "Basic #fsAuthorization#" },
	{ type: "header", name: "Content-Type", value: "multipart/form-data; boundary=#fieldBoundary#" },
	{ type: "body", value: requestBody.toList( "" ) }
];

cfhttp(
	method=fsMethod,
	charset="utf-8",
	url=fsURL,
	result="httpResponse"
) {
	for ( var httpParameter in httpParameters ) {
		cfhttpparam( attributeCollection = httpParameter  );
	}
}
```
I also agree that setting up a local proxy to log the raw http requests is a good idea to see what the differences are.
h
I set up an endpoint that I could send this to and dumped the GetHttpRequestData() but the content is just binary. Is there a better way to capture the request?
j
You could try something like this.
Copy code
content = isBinary(gethttprequestdata().content ) ? CharsetEncode(gethttprequestdata().content, "UTF-8") : gethttprequestdata().content ;
writeOutput("<pre>#content#</pre>");abort;
There may be a jvm flag to log them as well but don't recall what it is off the top of my head.
h
This is with the code I originally posted:
j
I would try it with the curl command and see what that looks like.
It doesn't appear to be handing the multipart there.
h
I'm not familiar with curl, but I tried it and am getting access denied. It doesn't seem to like my authorization. {"code":"access_denied","message":"You are not authorized to perform this action."}
is there a way to bypass the base64 encoding curl is trying to do so what I specify in the command line is passed through?
like -u mybase64encodedauthkey instead of -u username:password
j
I meant to hit your dummy endpoint to just see what the raw request looks like.
👍 1
h
I captured the request from curl so I'll see if I can adapt your code to the structure curl sent.
thanks for your code on manually building the request. I was able to get it to work, sort of. I got the content for curl to exactly match what I'm sending with CF and the file gets uploaded but it's mangled/unusable. I believe it's probably an encoding issue. The file size is always a little bigger after being uploaded to the service than it is locally.
j
No problem. In my case it was a csv. You would need to change this bit for a binary image.
Copy code
requestBody.append("Content-Type: text/csv#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( fileRead('e:\fsApi\attachments\#childData.ticket_id#\test.jpg', "utf-8") );
You might need something like this instead.
Copy code
requestBody.append("Content-Type: image/jpeg#NEW_LINE#");
requestBody.append(NEW_LINE);
requestBody.append( filereadbinary('e:\fsApi\attachments\#childData.ticket_id#\test.jpg', "utf-8") );
h
yeah, I did that.
I'll paste what I have in just a minute.
Copy code
requestBody.append('Content-Disposition: form-data; name="attachments[]"; filename="#filen#"#NEW_LINE#');
requestBody.append('Content-Type: #fileGetMimeType("e:\fsApi\attachments\#fsTicketId#\#filen#")##NEW_LINE#');
requestBody.append(NEW_LINE);
requestBody.append(fileRead('e:\fsApi\attachments\#fsTicketId#\#filen#'));
I'm using fileGetmimeType to auto set it.
in this last one, i took out the utf-8 and the filesize shrunk a bit but it's still bigger in the service than what it is locally.
curl on the left, ACF on the right. They look identical other than the boundary marker of course.
except ACF is sending a "Accept-Encoding" header where as curl is not.
i tried using a simple PDF and it uploads, opens as a pdf, but it's blank.
soooo close.
j
Makes me wonder if there is some whitespace character throwing it off.
h
hmm, no matter what I upload through the API, the service has the content-type as "application/octet-stream":
q
Can you make the call work with a tool like Postman? I'd try working with that first, then reverse engineering it from there. That way you can quickly iterate tests and see what is being sent back and forth. Also, if you can't get it to work with Postman, you at least have some ammo to go back to the API vendor and ask them to help get it working there...
h
Good idea. I found an article where someone created the request using PHP. They also have an example for Postman that I tried to follow. Verified I have basic functionality getting simple requests to work with Postman but trying to upload attachments with that doesn't seem to work either. I just get a response back with an error:
Copy code
{
  "message": "Invalid request parameters: invalid %-encoding ('' id='W5M0MpC...) , #)",
  "code": "invalid_encoding"
}
Not encouraging. I'm reaching out to Freshservice to see what they have to say.