Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[CAP] Timeout on long-running endpoint

In my application, I have a function that can take quite a long time to process, depending on the data selected. Two external systems were involved in the processing, so a lot of round trips were made. Of course, I tried to parallelize the calls to the external systems as much as possible, but it still took a long time. During the development in BAS everything worked fine, but during the deployment in BTP I encountered some errors, depending on the amount of data selected.

In the console I could see, that it was a 504 Gateway Timeout.

Luckily, the CAP docs are already explaining the possible reason for this. The approuter has a default timeout of 30 seconds for destinations. This matched my observation, that this issue only occurred when deployed.

https://cap.cloud.sap/docs/get-started/troubleshooting#why-are-long-running-requests-rejected-with-status-504-after-30-seconds-even-though-the-application-continues-processing-the-request

https://www.npmjs.com/package/@sap/approuter#destinations

In my case, the destination for my backend service is configured in the mta.yaml directly on the approuter. By simply adding the timeout property and by increasing the timeout from 30 seconds to 2 minutes, I could get rid of the errors.

  - name: my-approuter
    type: approuter.nodejs
    path: app/approute
    build-parameters:
      builder: npm-ci
      ignore:
        - "node_modules/"
        - "default-env.json"
        - "manifest*.yml"
    requires:
      - name: srv-api
        group: destinations
        properties:
          name: srv-api 
          url: ~{srv-url}
          forwardAuthToken: true
          timeout: 120000 # <--------------------------------- add timeout to your cap service destination
      - name: my-xsuaa
      - name: my-destination
      - name: my-html5-repo-runtime

[CAP] Posting form data to a destination using executeHttpRequest

        const { executeHttpRequest } = require('@sap-cloud-sdk/http-client')
        const FormData = require('form-data')

        try {
            //Create payload
            const form = new FormData()
            form.append('file', fileContent, {
                contentType: 'application/pdf'
                filename: 'test.pdf'
            })

            //Create headers
            const headers = {
                ...form.getHeaders(),
                'Content-Length': form.getLengthSync(),
            }

            //Send to Destination
            const response = await executeHttpRequest(
                { destinationName: 'TESTINATION' },
                {
                    method: 'POST',
                    url: 'myApiPath',
                    headers: headers,
                    data: form,
                    responseType: 'arraybuffer' // if you need the response data as buffer to prevent UTF-8 encoding
                }
            )
            console.log({response})
        } catch (error) {
            console.error(error.message)
        }

[CAP] Dynamically set destination in package.json for an external connection

  "cds": {
    "requires": {
      "sfsf": {
        "kind": "odata-v2",
        "credentials": {
          "destination": "<set during runtime>",
          "path": "/odata/v2",
          "requestTimeout": 18000000
        }
      }
    }
  },
  /*
   * Handover query to some external SF OData Service to fecth the requested data
   */  
  srv.on("READ", Whatever, async req => {

        const sf_api_def = cds.env.requires['sfsf'] //defined in package.json

        sf_api_def.credentials.destination = "myDestinationName" //set your Destination name, could come from a customizing table

        const sfsfSrv = await cds.connect.to(sf_api_def)

        return await sfsfSrv.run(req.query)
    })

[CAP] Providing destinations locally using .env

https://sap.github.io/cloud-sdk/docs/js/features/connectivity/destinations#local-environment-variable

The provided example

destinations="[{\"name\": \"TESTINATION\", \"url\": \"http://url.hana.ondemand.com\", \"username\": \"DUMMY_USER\", \"password\": \"EXAMPLE_PASSWORD\"}]"

didn’t work for me. I had to change it to the following format:

destinations=[{"name": "TESTINATION", "url": "http://url.hana.ondemand.com", "username": "DUMMY_USER", "password": "EXAMPLE_PASSWORD"}]

You can check the current value using:

process.env.destinations

[CAP] Use Destinations from Subscriber Account

While working on this topic, there are currently two chapters in the CAP docs regarding this topic:

And some SCN posts: herehere and here

But all this didn’t help me to complete the task. Got the solution which finally worked for me from this SAP Sample Susaas (two snippets: here and here).

server.js

const cds = require("@sap/cds")

cds.on('served', async () => {
    const { 'cds.xt.SaasProvisioningService': provisioning } = cds.services
    // Add provisioning logic only if multitenancy is there..
    if (provisioning) {
        let tenantProvisioning = require('./provisioning')
        provisioning.prepend(tenantProvisioning)
    } else {
        console.log(">>> There is no service, therefore does not serve multitenancy!")
    }
})

module.exports = cds.server

provisioning.js

const xsenv = require('@sap/xsenv')
xsenv.loadEnv()

module.exports = (service) => {

    service.on('dependencies', async (req, next) => {
        let dependencies = await next()
        const services = xsenv.getServices({
            registry: { tag: 'SaaS' },
            destination: { tag: 'destination' }
        })
        dependencies.push({ xsappname: services.destination.xsappname }) //adds the subscriber destination as dependency
        console.log(">>> SaaS Dependencies:", JSON.stringify(dependencies))
        return dependencies
    })

}

It injects the destination dependency by manually reading it using xsenv package and returning it in a dependencies callback handler.