'Coldfusion cfhttp multipartType - How do I send JSON and file in same request?

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.



Solution 1:[1]

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 ....

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1