Technical Articles
What is form-data and how to send it from SAP Cloud Platform Integration (CPI)
Disclaimer
This blog is meant for documentation/educational purposes only.
It is the result of our findings and no guarantee that the same solution will work in all cases. I was informed that multipart sending/form-data is not officially supported by SAP Cloud Integration.
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 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.
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
Nevertheless, your blog highlights some of the challenges and what to look for in form-data interfaces, and that is really useful 🙂
The whole idea of comparing the payload with Postman is a useful troubleshooting approach. Another approach that I would recommend (which I used myself in the above case) was to route the message through a proxy that captures the details of the HTTP call. You can find out more in the following blog - that has been a life saver many times round.
https://blogs.sap.com/2019/01/01/https-tracing-and-debugging-4-cloud-to-cloud-using-cloud-foundry/
Regards
Eng Swee
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.
Kind regards,
Nick
Hi Nick,
thank you for your comment!
That's a strange error, glad you could fix it - the only thing is that doing HTTP calls from groovy is really discouraged...
Best regards
Matti
Hello Nick,
This is really interesting, because I'm running into the exact same problem; the Content-Length not defined error.
I've tried nearly everything, but with no success.
My current script is very similar to the one Eng Swee posted in the other blog, but I'm really curious in understand your approach.
At the moment I'm testing your script and I'm running into the following error:
java.lang.NoSuchMethodException: No signature of method: sun.net.www.http.PosterOutputStream.write() is applicable for argument types: (org.apache.camel.converter.stream.InputStreamCache) values: [org.apache.camel.converter.stream.InputStreamCache@144c18fd] Possible solutions: write([B), write(int), write(int), write(int), wait(), wait(long)
I'm using SFTP Adapter to pickup the file to send to Ariba. The only thing in the iflow is the Script Process.
Can you kindly give your opinion on this matter?
Thank you in advance.
João
Hi João,
Based on error message, the type of input parameter in below code is unacceptable.
The type of variable zipContent in my script is byte array and it's the output of combination of form + binary content (zip file). If you want to discuss further, then create a question post and I'll answer in the new post.
Kind regards,
Nick
Hello Nick,
I've created another question. It's on this URL:
https://answers.sap.com/questions/12963572/send-zip-file-and-form-data-through-cpi-and-http-a.html
Thank you in advance for providing your insights on this topic.
Kind regards,
João Gomes
Hello Matti,
Thank you so much for the blog.. I have similar kind of requirement. However, I need to pass as below with the following Body in form-data. Kindly help on the same.
Hi Prashanth,
you can probably just use the OAuth Adapter, Type: Client Credentials to do this type of POST request.
If it needs to be form-data, you can refer to my guide above and add your key value pairs accordingly. In Postman you can show the final request by clicking on "Code" next to the Send Button or "</>" in the newest version of Postman.
Best regards
Matti
At Content Modifer Header Content-Type = multipart/form-data; boundary=cpi then in the Content Modifier body specify below. Note that I have externalized Client id and secret.
Hello Matti,
Thank you so much for the reply. I have tried as below(attached Screenshot). Please correct me if the following parameters are incorrect. Currently, I am facing 400 bad request error. Kindly help.
Content-Modifier-Header
Content Modifier- Body
Regards,
Prashanth Bharadwaj.Ch
try like below in the Content Modifier body
Hello Matti,
Thank you so much for the blog. I have similar kind of requirement, but it does not work:
I am facing 400 bad request error. Kindly your help please.
Regards,
C.Buriticá
Great work Matti Leydecker
Code for sending file in form data.
Reference: https://answers.sap.com/questions/12716088/sap-cpi---forwarding-raw-image-data-through-integr.html
Regards,
Sunil
Hi Sunil,
thanks for sharing the code example.
In my Use case I wanted to uplaod a csv file, therefore I had to add one parameter to make the call successful:
Content-Type: text/csv else I got the error:
{"Message":"No CSV file received or invalid extension."}
Regards