Skip to Content
Technical Articles

What is form-data and how to send it from SAP Cloud Platform Integration (CPI)

Introduction

Our team solved an interesting challenge involving SAP Cloud Platform Integration and an API expecting to receive form-data today.

We faced an API which accepted a POST request using the Postman tool but would always throw an error when ‘exactly the same content’ (we will learn more about that later) was sent from the CPI.

Because the POST request needed to send form-data, this strange behaviour made us dig into the formal definition of form-data by W3C. We also found some very nice blogs like this one by Pieterjan who explains form-data from a little bit different angle.

In this blog, we will explain the concept of form-data, how to send it from Postman and how to send it from the Cloud Platform to another system.

What is form-data anyway?

Form-data originates from HTML forms which take user input and send it through the browser to a web server. The same technology can be used to send data between applications other than browsers.

It comes in two flavours: application/x-www-form-urlencoded and multipart/form-data. Both are used to send key-value mappings. In HTML forms, a field is represented as a mapping of the name of the field to the content. 

Urlencoded form-data is the standard way but not sufficient for sending large quantities of binary data also known as files. For more information see the W3C definition.

Two ways of sending form-data

The rest of this blog will talk about the content type multipart/form-data. Don’t be scared by the multipart, it also works if your request contains only one part.

First, we will explore how to send the form-data through Postman. This is easily done with the below settings. The body is set to form-data and key-value pairs can be added. Note that it is possible to add simple strings, formatted data like JSON and also files as values.


Postman takes care of the rest behind the scenes. To recreate this request in the SAP Cloud Platform Integration we first recreated this request in Postman using the raw content type. This is needed as the CPI does not support form-data directly but has only the basic capabilities to alter header and body.

The information we needed to enter was gathered from the W3C definition in combination with Postman’s handy feature to generate code from a request by clicking on Code (in the top right corner of the screenshot above). The details will follow in the next section.

Header and body

Two things are important in the generated code to create the form-data: the header Content-Type and the body formatting.

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

The header labels the content as form-data and introduces the boundary, which is a user-chosen string. Postman generates this one for us. Don’t worry about the multipart, this is used for all form data, even if it contains only one part like in our examples.

Content-Disposition: form-data; name="formElement"

ExampleValue
------WebKitFormBoundary7MA4YWxkTrZu0gW--

The body declares form-data again but also gives the data a name, this is the key in the Postman example in the previous section. We also see our “ExampleValue” which is the value in the said example. Please note that the boundary in the body has two more dashes “–” in the beginning than the boundary declared in the header. This is the rule per the definition.

More key-value pairs could be added in the body here but let’s keep it simple for now. The very last boundary needs to also have to dashes in the end (compared again to the boundary in the header).

 

To the Cloud Platform Integration

In the CPI you would now go ahead and create the header:

With the value (note the much simpler but still working boundary):

Content-Type: multipart/form-data; boundary=cpi

and the body:

 

Content-Disposition: form-data; name="formElement"

ExampleValue
--cpi--

Normally our story would end here. The CPI sends out the well-formatted form-data and we are happy.

The anomaly

In our case, the API we were trying to talk to would accept the form-data from Postman with either way, raw or as form-data. If we copied the exact same header and body which worked in Postman to the CPI we’d get an HTTP 400 error “Unable to parse multipart body”. This happens for example when you leave out the newline between Content-Disposition and the ExampleValue in the example above.

After observing different payloads over and over again we tried not to create the form-data in the CPI but send it there from Postman and route it through to the API. Normally for our use-case, we need to send a SOAP message but as nothing was working this seemed like something we could give a try.

And it was successful. With the message from Postman routed through the CPI, the API accepted the request. So we checked what the difference was between the request we created “by hand” in the CPI (see chapter To Cloud Platform Integration) and the one coming from Postman.

Even comparing them side by side in the CPI tracing view showed no difference. We also created an Iflow that stripped away all headers but the Content-Type but with no luck.

Finally, we compared the two payloads in Notepad++ which can show hidden characters like line ending characters (View -> Show Symbol -> Show All Characters). It seems Postman on Windows creates the “correct” line ending characters and the CPI creates UNIX line endings which our API could not handle. Turns out it wasn’t ‘exactly the same data’.

The solution

To solve our problem we created a script which would automatically convert the linefeeds. You can find it below should you ever run into this kind of trouble.

import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
def Message processData(Message message) {
    //Body 
       def body = message.getBody(String);
       body = body.replaceAll("\n", "\r\n");
       body = """--cpi\r\nContent-Disposition: form-data; name="envelope"\r\n\r\n\r\n""" + body + """\r\n\r\n--cpi--"""

       message.setBody(body);
       return message;
}

Conclusion

In this blog we explained what form-data is, showed the different forms and how to send it through Postman. Then we moved to the Cloud Platform Integration and sent form-data from there. In the end we showcased an error with an API that threw an error on UNIX line endings and how to fix it.

Thank you for reading! Please let me know if you faced similar problems or if you have further questions regarding form-data requests or other CPI topics.

4 Comments
You must be Logged on to comment or reply to a post.
  • Hi Matti

     

    Thanks for sharing this. Few months back, there was an interesting question that came up in the forums that had to post form-data to Leonardo’s MFLS OCR service.

     

    Forming the form-data part with Content Modifier did not work in that case because the content was binary, and the Content Modifier would corrupt the content because it would undergo conversion to string.

     

    Here’s my solution for that case using Groovy Script for that case, which could cater for both binary or text content. Might be of interest to you 🙂

     

    Best regards,

    Eng Swee

     

    • Hi Eng Swee,

      while searching for an answer for the anomaly described in this blog, I came across your answer on that particular problem!

      Naive that I was at that point I thought it can’t be that complicated – having to compare files on a binary level. Turns out I was wrong and we had to dig quite a bit into the file/encoding level here.

      I will keep this in mind going forward and use some parts of your script for convenient multipart sending 🙂

      Thank you for your comment.

       

      Best,

      Matti

  • Hi Matti,

    I encountered very much exactly same issue early this year like you experienced in this blog.

    I was trying to post zip file inside multipart form-data to Ariba Network server from CPI.

    The content produced by my groovy script looks exactly same as same one from PostMan but server always returned “invalid content length”. I suspected HTTP adapter might work in a different way than I thought.

    In the end, my work around solution is to post form and binary content through groovy script like below and it works.

    def Message postZipContentToAriba(Message message){
        def messageLog = messageLogFactory.getMessageLog(message)
        def propertiesMap = message.getProperties()
        def headersMap = message.getHeaders()
        // POST
        def post = new URL(propertiesMap.get("lAriba-Network-URL")).openConnection();
        def zipContent = message.getBody()
        
        post.setRequestMethod("POST")
        post.setDoOutput(true)
        post.setRequestProperty("Content-Type", headersMap.get("Content-Type") as String)
        post.getOutputStream().write(zipContent);
        
        def postRC = post.getResponseCode();
        if(postRC.equals(200)) {
            messageLog.addAttachmentAsString("Post result:", post.getInputStream().getText() as String, "text/plain")
        }else{
            def exceptionMsg = "HTTP" + postRC.toString() + ", " + post.getInputStream().getText()
            throw new Exception(exceptionMsg as String)
        }
        
        return message
    }

     

    Kind regards,

    Nick