1

I'm up against an API that has very sparse documentation. I've tried everything (more below) but the end result is a response from the 3rd party server: The given path's format is not supported.

Here are my requirements for this endpoint (POST):

  • A multipart/form-data POST
  • A formfield named json with a JSON string
  • A file named file

Here's the "documentation" I was given (simplified the JSON for clarity)...

---------------------------acebdf13572468
Content-Disposition: form-data; name="json"
Content-Type: application/json

{
  "Foo": "bar",
  "Bar": "foo"
}

---------------------------acebdf13572468
Content-Disposition: form-data; name="file"; filename="api.jpg"
Content-Type: image/jpeg

<@INCLUDE *C:\Users\johnSmith\Pictures\api.jpg*@>
---------------------------acebdf13572468--

I set up a page on my server so instead of posting to their API, I post to my page so I can see the data that is being posted. I did this because despite my best attempts, I can't get the 3rd party to retrieve the logs to tell me what they are seeing on their end.

Here's the code which produced the output that most closely resembles their "code sample" I pasted above:

<cfhttp method="POST" url="#ApiUrl#" result="CfhttpResult" timeout="30" multipart="yes" multipartType="related">
    <cfhttpparam type="header" name="subscription-key" value="#SubscriptionKey#" />
    <cfhttpparam type="header" name="developer-key" value="#DeveloperKey#" />
    <cfhttpparam type="formField" name="json" value="#Payload#">
    <cfhttpparam type="file" name="file" file="#FilePath#" mimetype="#FileMimeType#">
</cfhttp>

Here's the result from posting to a page on my server instead of the API:

-------------------------------7d0d117230764
Content-Disposition: form-data; name="json"
Content-Type: text/plain; charset=UTF-8

{"Foo": "bar","Bar": "foo"}
-------------------------------7d0d117230764
Content-Disposition: form-data; name="file"; filename="E:\WEBROOT\mywebsite.com\wwwroot\content\file\2022\05\test_024808PM.png"
Content-Type: image/png

�PNG
 
   
IHDR   �   �     X' �  
�iCCPICC Profile  H��� TS� �Ͻ鍒�k轷 RB �  D%$�� CBP�+�#8���` ��*8*EƂX�0(6� dPQ��  *� 

(loads more binary data here)

To me, it looks spot on and matches their example, but I'm still getting the same response.

The multipartType attribute seemed to be the key, something I have not used before after ~14 years writing ColdFusion. Seems like it adds the necessary headers and separates the file from the JSON.

Can anyone spot something I may be overlooking? I'm desperate for another pair of eyeballs and a sanity check.

jyoseph
  • 5,435
  • 9
  • 45
  • 64
  • 1
    One difference that jumps out at me is the sample `filename` value is just a file name and extension `"api.jpg"` but the cfhttp value includes the directory path and name `"E:\WEBROOT\mywebsite.com\wwwroot\content\file\2022\05\test_024808PM.png"`. Based on the error message I'd guess the receiving end uses C# and [Path.Combine](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.combine?view=net-6.0) to construct a path to the uploaded file. (cont'd) – SOS May 10 '22 at 20:25
  • 1
    If it lacks error handling it might crashes when it processes your file because `filename` contains a directory path where it wasn't expected, ie. `Path.Combine("c:\localfolder\", "E:\WEBROOT\mywebsite.com\wwwroot\content\file\2022\05\test_024808PM.png")`. – SOS May 10 '22 at 20:25
  • 1
    (Edit) Just for grins, try the suggestions in [this thread](https://stackoverflow.com/a/7407179/8895292) 1) As a test use the apache httpClient to do the POST. See if the api response differs 2) Try adding a formfield named "fileName" and set the value to the file name and extension . FWIW, including the full path seems to be a peculiarity of ACF. I tried the same code under Lucee and cfhttp did *not* include the full path. – SOS May 11 '22 at 04:11
  • 1
    Thank you so much for your suggestion @SOS I will try that now. Yeah I'm really confused as to why the full path is listed. They are not actually fetching the file from our server, so it seems... unnecessary? – jyoseph May 11 '22 at 18:31
  • 1
    Yeah, since the POST already contains the file binary I'm struggling to find a good reason why cfhttp should ever be sending full paths to remote url. Not a great security choice, exposing local file paths ... – SOS May 11 '22 at 18:54
  • I tried the java solution from the thread you linked. Thank you for that, by the way. Unfortunately, no luck as it doesn't seem like the binary data is being posted. If I dump `GetHttpRequestData()` on the receiving page, it appears there is binary data in the content, but when I look into that (`GetHttpRequestData().content` or `CharsetEncode(GetHttpRequestData().content,"utf-8")` then it's empty. It's infuriating that something so silly is completely stopping the show here. I can get it to work in Postman, just not CF. – jyoseph May 11 '22 at 20:14
  • 1
    So did you try posting to the API, or just the test cfm page? Because `content` can be empty in certain cases, i.e. it doesn't always indicate a problem. – SOS May 11 '22 at 20:21
  • Correct, yes I did post to the API. I only see the status code 401 returned when posting to the API. I wasn't sure on how to get the full response from that java example. On top of that, the API requires a field named `json` as well, and I wasn't sure how to update that code to include it. For as long as I've been writing CF, I'm embarrassed to say I've only had to dip into java a few times (also to get around API related issues). – jyoseph May 11 '22 at 20:36
  • Started digging into Postman a bit more, where I can successfully make a POST to the API. There is an option to generate code snippets. This was the option for cURL... ``` curl --location --request POST 'apiurlhere' \ --header 'Subscription-Key: 12345678' \ --header 'Developer-Key: 12345678' \ --form 'json="{\"Foo\":\"Bar\",\"Bar\":\"Foo\"}";type=application/json' \ --form 'file=@"/Users/jyoseph/Desktop/test.png"' ``` It seems so simple that way. – jyoseph May 11 '22 at 20:37
  • Well, shoot, that didn't work. Here's the cURL command: https://pastebin.com/raw/RFcXDpSf – jyoseph May 11 '22 at 20:39
  • 1
    I don't have curl on my current machine, but try this example. Ran it through Fiddler and see the expected headers and fields. https://pastebin.com/FVL2wxkj – SOS May 11 '22 at 22:33
  • 1
    Why is your JSON Content-Type: text/plain when Content-Type: application/json is expected? Did you try the correct mimetype? – AndreasRu May 12 '22 at 00:30
  • 1
    @jyoseph - Didn't work how, the api still returns a 401? I tried it with curl and it works, in the sense that a) Lucee and ACF receive both fields and save the file successfully and b) Fiddler shows the expected headers, field names and values (see pastebin above). Though the Content-Types differ based on the client used (curl, cfhttp or httpClient). – SOS May 12 '22 at 00:59
  • 1
    @SOS Wow, I can't believe it, but this problem is finally solved thanks to your last reply with the pastebin link! Thank you so much for sticking with me and trying different solutions. I don't usually ask on SO unless I'm at my wits' end, and this was one of those times. I've been struggling with this on and off for a few weeks for a total of about 5 entire working days. Will you please add that as an answer so I can accept it? – jyoseph May 12 '22 at 12:05
  • 1
    @jyoseph - Great! I've been there myself, so I'm glad I could return the favor this time :-) If you ever get some free time, I'd be curious to know the results using cfhttp under Lucee (which doesn't submit a full file path). It's relatively easy using [CommandBox](https://www.ortussolutions.com/products/commandbox). Just download and spin up a Lucee server `start name=Lucee5.3.9.133 cfengine=Lucee@5.3.9.133`. If the cause does turn out to be the full file path, you might want to [submit a bug report](https://tracker.adobe.com/) to Adobe. – SOS May 12 '22 at 17:29
  • 1
    I've never heard of CommandBox, I feel like I've had my head in the sand. It looks awesome, though. TIL! I'm downloading it now. I could see having a lot of fun with this. I'll post back here when I find something out because you're right, this might be a bug. HEY, also, don't forget to add an answer, I want to make sure you get credit. I'd buy you a coffee (or a weeks worth of coffee) if they had that functionality here! – jyoseph May 12 '22 at 17:56
  • 1
    Heh, you're not the only latecomer. I'd heard of it, but hadn't played around with it until recently. It's pretty sweet. Makes testing different versions SO much easier. Once you start `box.exe/sh` just create a directory for the new server and navigate into it. Then run the `start {servername} {version}....` command and boom - you're up and running. Do let me know what you find out! .. and thanks for the virtual coffee offer :-) – SOS May 12 '22 at 21:24

1 Answers1

3

One difference that jumps out at me is the sample filename value only contains a name and extension:

Content-Disposition: form-data; name="file"; filename="api.jpg"

Whereas the one from cfhttp includes a directory as well. (FWIW, this seems to be a peculiarity of Adobe ColdFusion. Executing the same code under Lucee does not include the directory. Since the POST already contains the file binary I'm struggling to find a good reason why cfhttp should ever be sending full paths to remote url. Not a great security choice IMO, exposing local file paths ... )

Content-Disposition: form-data; name="file"; filename="E:\WEBROOT\mywebsite.com\wwwroot\content\file\2022\05\test_024808PM.png"

Anyway, based on the error message I'd guess the receiving end is using C# and Path.Combine to construct a path to the uploaded file. It might be crashing when processing your file because the filename value contains a directory path where it wasn't expected?

Just for grins, try the suggestions in this thread which suggests using the Apache's HttpClient to perform the POST instead of cfhttp. Try it and see if the API response differs. Here's an updated version based on your fields:

<cfscript>
    // Note: Using port 8888 for Fiddler
    postURI = "http://127.0.0.1:8888/dev/testPage.cfm";
    filePath = "c:\temp\sample_plan.png";
    payload = serializeJSON({"something" : "here"});
 
    method = createObject("java", "org.apache.commons.httpclient.methods.PostMethod").init( postURI );
    
    try {
        // add custom headers
        method.addRequestHeader("subscription-key", "keyvalue-123");
        method.addRequestHeader("developer-key", "devkey-456");
 
        // add "json" form field
        jsonPart = createObject("java", "org.apache.commons.httpclient.methods.multipart.StringPart").init(
            "json"
            , payload
        );
    
        // Optional, use if you need to set the content-type to "application/json"
        // jsonPart.setContentType("application/json");

        // add "file" field
        filePart = createObject( "java", "org.apache.commons.httpclient.methods.multipart.FilePart").init(
            "file"  
            , "myFile.png"
            , createObject( "java", "java.io.File").init( filePath )
        );

        // construct request data     
        requestEntity = createObject( "java", "org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity").init(
            [ jsonPart, filePart  ]
            , method.getParams()
        );
    
        method.setRequestEntity( requestEntity );
        
        // submit and display response
        status = createObject('java', 'org.apache.commons.httpclient.HttpClient').init().executeMethod( method );
        body = method.getResponseBody();
        writeOutput( charsetEncode(body, "utf-8"));
    
    }
    finally {
        method.releaseConnection();
    }
</cfscript>

Since GetHTTPRequestData() doesn't always provide the unadulterated request data, here's the raw response from Fiddler. It appears to contain the correct fields and headers:

POST /dev/testPage.cfm HTTP/1.1
subscription-key: keyvalue-123
developer-key: devkey-456
User-Agent: Jakarta Commons-HttpClient/3.1
Host: 127.0.0.1:8888
Content-Length: 100628
Content-Type: multipart/form-data; boundary=f5H4CnWAvpMQPoK_X7J2YKmgiN_gAnn
 
--f5H4CnWAvpMQPoK_X7J2YKmgiN_gAnn
Content-Disposition: form-data; name="json"
Content-Type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 8bit
 
{"something":"here"}
--f5H4CnWAvpMQPoK_X7J2YKmgiN_gAnn
Content-Disposition: form-data; name="file"; filename="myFile.png"
Content-Type: application/octet-stream; charset=ISO-8859-1
Content-Transfer-Encoding: binary
 
�PNG
... more binary data ....
SOS
  • 6,430
  • 2
  • 11
  • 29
  • 2
    Again @SOS, you are a great supporter of our cfml community. Thanks for posting all that answers and explanations in that well written and understandable manner. – AndreasRu May 12 '22 at 21:24
  • 2
    @AndreasRu - Much appreciated! I can return the compliment as your well thought out and detailed answers really benefit the community :-) – SOS May 12 '22 at 21:38
  • @SOS I'm extremely grateful for your help! Your code sample was right on the money. I'm going to tinker around with CommandBox for sure. I'm kind of interested to use a docker container and maybe build a local app to manage family stuff like kids chores, events, etc. Anyway, thank you again! – jyoseph May 13 '22 at 23:06
  • @jyoseph - Glad I could help, and yep... lots of fun possibilities with CommandBox :-) – SOS May 16 '22 at 02:15