Skip to Content
Technical Articles

Returning a Buffer/File from CAP

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.

We were, as I’ve mentioned, using CAP on NodeJS and trying to return a file we created in JSPDF. It sounds easy… initially we settled for asking JSPDF to produce a BASE64 String (as apart of a built-in feature included in JSPDF) and returned that as a String to the client before one of our customers pointed out in testing that our implemented solution didn’t work on iOS devices. I’ll clarify that the BASE64 String was returned as a DataURI String from our CAP server. On most desktop browsers we were able to create an <a> element, link to the DataURI String and include the HTML download attribute, fire .click() on that element and the file would download (as per StackOverflow’s recommendations). 

let link = document.createElement('a');
link.href = data_uri_string; = `${file_name}.pdf`;;

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 “” to open the DataURI String in a new tab, in the same tab, and even using the reference object returned by “” 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.

I’d say you are wondering why we didn’t just return the File from CAP in the first-place? Well… as far as we could tell CAP has a very specific way it wants us to handle files… it wants to retrieve them from a database. We turned the SAP Community upside down trying to find a solution but all existing blog posts and CAP documentation only had examples for creating a service, storing the file in the database and using OData to retrieve the “content” of the file from the database. It seems unnecessary for us to create this file, store it in the database, provide a limited window for the client to download the file and then delete it from the database. We weren’t provisioned the resources to store the file and even if we did have the resources the dynamic nature of our Application would make this a taxing process – I won’t divulge too many details but creating the file in real-time was our only real option.

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 found this super frustrating, since CAP was built on ExpressJS I should easily be able to send something without OData… but, and please correct me in the comments if I am wrong, it doesn’t seem CAP is capable of this.

Edit: David has mentioned in the comments that it is possible to register an Express handler inside the CDS handler for the ‘bootstrap’ event. While this was known at the time, as David mentioned it doesn’t implement CAP’s security – a dealbreaker for us in this circumstance.

I had a bit of a eureka moment at this point and too was delighted to see the in the CAP documentation that the underlying Express Response was accessible via the “req” parameter passed to event handlers. I wondered if I could steal the response away from CAP and send my own response as if CAP and it’s underlying middleware didn’t exist for this endpoint. 

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()”.

It’s worth noting here, as noted in the CAP documentation, that CAP stores a reference to the Express response object in “req._.res” (where req is the request parameter passed to your handler event).


req._.res.set(‘Content-Type’, ‘application/pdf’);

Once we had set the content type, all we had to do was return the Buffer:


We have to ensure that the method ends here… the server will throw an error if CAP tries to manipulate the request after it’s been sent… so no return statement or req.reply() statements can follow. 

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');
    } 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 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 && {
    let sPath =“/generate_pdf(…)";
    let sOrigin = window.location.origin;
    let sServiceUrl = oModel.sServiceUrl;`${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; = `${file_name}.pdf`;;
    }).catch((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.

I hope this blog post was informative and if you have any questions feel free to leave a comment!

If you’re interested in learning more about CAP there is a Q&A available here >>
Or you can view the SAP Community topic on CAP here:

You must be Logged on to comment or reply to a post.
  • Hi Lachlan,

    I found this super frustrating, since CAP was built on ExpressJS I should easily be able to send something without OData… but, and please correct me in the comments if I am wrong, it doesn’t seem CAP is capable of this.

    It is possible to register express handlers in your CAP app:

    // custom server.js
    const cds = require('@sap/cds')
    cds.on('bootstrap', app => {
      // register handlers/middlewares using the express app
    module.exports = cds.server

    More info here:

    Note: As it's a custom handler, you're responsible for its security.


    Best regards,

    • Hi David,

      I did discover this at some point but we use the CDS security (incl. XSUAA integration) and didn't want to have to re-implement our security for specific endpoints (as you've noted).

      Thanks! 😊

    • Hi David,

      I have a function import implementation in CAP's nodejs runtime where the function processes few tasks that run for long and I want to send back some updates to the user via req.res.send(""). Our API is being simply consumed in POSTMAN to do some batch works behind the scenes  and its a valid use case 🙂

      But as Lachlan pointed out doing this will essentially interfere with CAP's processing unless I terminate the request right after I execute the send() method.

      Is there a workaround where I can send multiple responses back piggybacking on the underlying HTTP response object without needing to terminate the request and won't end up getting this error:

      [cds] - {
      message: 'Headers already sent',
      id: '1156639',
      level: 'WARNING',
      timestamp: 1617824793400
      [cds] - InternalServerError: Response data was already sent while there is still data available in response buffer
      at SendResponseCommand.execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/SendResponseCommand.js:30:13)
      at CommandExecutor._execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:74:17)
      at /home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:84:18
      at SerializingCommand.execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/SerializingCommand.js:39:14)
      at CommandExecutor._execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:74:17)
      at /home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:84:18
      at SetStatuscodeCommand.execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetStatuscodeCommand.js:46:7)
      at CommandExecutor._execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:74:17)
      at /home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js:84:18
      at SetResponseHeadersCommand.execute (/home/user/projects/IAC-Reporting/node_modules/@sap/cds-runtime/lib/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js:87:5) {
      id: '1156639',
      level: 'ERROR',
      timestamp: 1617824793401