Today, I will outline the steps our team took to return a File from CAP under some unique circumstances. While this tutorial follows the story of how to return a raw PDF File created on-the-fly in CAP on NodeJS, the underlying logic pertains to any file whether it has to be retrieved from an external service, the file system or (like our case) created in real-time.
let link = document.createElement('a');
link.href = data_uri_string;
link.download = `${file_name}.pdf`;
link.click();
While this worked a treat on Chrome, Firefox and even Safari on Windows and MacOS, our customer uses iPad devices in the field and Safari on iOS didn’t like this. We soon learnt that Safari on iOS doesn’t like a lot of things… we tried using JavaScript’s “window.open” to open the DataURI String in a new tab, in the same tab, and even using the reference object returned by “window.open” to construct a temporary page that included a real, rendered <a> link that the user would click on to download the file (so the browser would attribute a user-action to us trying to download a file). Safari hated all of this, it would not open the file no matter how hard we tried and the results on Google and StackOverflow confirmed our suspicions that Safari’s strict security requirements on iOS made this overly difficult and in a lot of cases, impossible.
We tried to specify entities that returned LargeBinaries and MediaTypes but CAP always parsed our File into some-kind of encoded String, wrapped it in an OData Response and set the Content-Type header to application/json - which the browser cannot parse as a File.
I could! If I didn’t return anything from the event handler, or call any helper methods like req.reply, but instead called the Express .send() method, I was able to take control of the response and have Express fulfil the request rather than CAP.
Now, JSPDF is meant to be used as a client-side library so a lot of it’s returned types only apply to JavaScript API’s available in the browser: ArrayBuffer and Blob doesn’t exist in Node… however Node does have the Buffer type. It's similar to ArrayBuffer and easily able to take an ArrayBuffer and convert it into a Buffer.
let array_buffer = generate_pdf();
let buffer = Buffer.from(array_buffer);
If you’ve ever read the Express documentation, you’d know Express handles Buffer’s natively and you can pass one to the “res.send()” method in Express and it will do the rest. I discovered however at this point that CAP had already set the Content-Type header as application/json and in the browser I was receiving the encoded String the browser parsed as JSON. No worries... we have access to the response object so we can just set the Content-Type header to “application/pdf” before we call “res.send()”.
req._.res.set(‘Content-Type’, ‘application/pdf’);
req._.res.send(buffer);
All together, our server-side code looked like this:
NOTE: We did this inside the event handler for a “CAP function” that was declared in our CDS file - this way we can ensure that all of our security configurations remain in-tact and we keep our API consistent.
srv.on(‘generate_pdf’, async req => {
try {
let array_buffer = generate_pdf();
let buffer = Buffer.from(array_buffer);
req._.res.set('Content-Type', 'application/pdf');
req._.res.send(buffer);
} catch (error) {
req.reject(400, error);
}
});
We then used UI5’s built in device model to determine whether or not the client was being accessed from a mobile Safari device, constructed the URL and called JavaScript’s window.open API to open the file in a new tab, calling the generate_pdf() back-end function and displaying a PDF the user was now able to see and save.
if (sap.ui.Device.browser.safari && sap.ui.Device.browser.mobile) {
let sPath =“/generate_pdf(…)";
let sOrigin = window.location.origin;
let sServiceUrl = oModel.sServiceUrl;
window.open(`${sOrigin}${sServiceUrl}${sPath}`, "_blank");
} else {
let sPath =“/generate_pdf_alt(…)";
let oBindingContext = oModel.createBindingContext("/");
let oOperation = oModel.bindContext(sPath, oBindingContext);
oOperation.execute().then(() => {
let response = oOperation.getBoundContext().getObject();
let data_uri_string = response.value;
let link = document.createElement('a');
link.href = data_uri_string;
link.download = `${file_name}.pdf`;
link.click();
}).catch((error) => {
console.error(error);
});
}
Of course, this solution works as long as you have a Buffer (or have something that can be converted into a Buffer). It doesn’t need to come from JSPDF, it can come from an external source or even the File System. It’s easy to translate files into a Buffer in NodeJS - Buffer.from() is particularly powerful but there’s also plenty of NPM packages available for more obscure cases.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
9 | |
4 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 | |
3 | |
3 |