Skip to Content
Technical Articles

Consuming destinations in cloud foundry using axios in a nodejs application

Hi all,

There are a lot of http clients available to consume HTTP service in Javascript.
There are a lot of discussions on which one is the best, I love them all, but in the end you have to use only one.

My personal favorite is axios. it’s well documented, supports many features and is easy to learn.
That’s why I decided to make the life of the axios users in the SAP Cloud Foundry easier and created a sap-cf-axios library.

Let’s start with explaining all different parts we need to make an http call.

Destinations

Destinations are defined in the cloud cockpit.
We use these destinations to configure our connection:

  • a base url
  • authentication configuration
  • proxy configuration

Using the destination in a nodejs applications requires a binding to a destination service. If the destination is pointing to an on premise system we have to connect to this service via the cloud connector. To call a service over the cloud connector we have to adapt our proxy settings in our http client. To read the configuration of the proxy and the authentication information, we need to bind our application to a connectivity service.

In my example here I define 3 services (UAA, destination and connectivity) and I bind the services to my approuter and service.

ID: sap-cf-axios-example
_schema-version: "2.1"
description: Example sap cf axios
version: 0.0.1
modules:
  - name: sca_approuter
    type: approuter.nodejs
    path: approuter
    parameters:
      disk-quota: 256M
      memory: 256M
    requires:
      - name: sca_uaa
      - name: sca_destination_service
      - name: sca_api
        group: destinations
        properties:
          forwardAuthToken: true
          name: sca_api
          strictSSL: false
          url: '~{url}'
  - name: sca_srv
    type: nodejs
    path: srv
    parameters:
      memory: 512M
      disk-quota: 256M
    provides:
      - name: sca_api
        properties:
          url: ${default-url}
    requires:
      - name: sca_uaa
      - name: sca_connectivity_service
      - name: sca_destination_service
resources:
  - name: sca_destination_service
    type: destination
    parameters:
      service-plan: lite
      shared: true
  - name: sca_connectivity_service
    type: connectivity
    parameters:
      service-plan: lite
      shared: true
  - name: sca_uaa
    type: com.sap.xs.uaa
    parameters:
      service: xsuaa
      service-plan: application
      path: ./xs-security.json

Axios

https://github.com/axios/axios

When we use axios in nodejs we have to create an AxiosRequestConfig object that specifies the properties like method, url, payload, headers of the call we need to send to our service.

The return of the axios function is a promise with the AxiosResponse object where we can read the response header and the body among other things. Let’s give an example of an odata service served on https://my.site/myService.

If we want to post a Book entity to this service that requires authentication we can use this code:

const axios = require('axios');

const response = await axios({
  method: "post",
  url: "/BookSet",
  baseUrl: "https://localhost:3002"
  data: {
    title: "Using Axios in SAP Cloud Foundry",
    author: "Joachim Van Praet"
  },
  headers: {
    "content-type": "application/json"
  },
  auth: {
    username: 'jowa',
    password: 's00pers3cret'
  }
})

Axios with destinations

To use axios with the destination and connectivity service you can install the sap-cf-axios in your application.

npm install -s sap-cf-axios

To keep the use of axios as close as possible to the standard. I decided to create an axios instance for each destination. For each destination a request interceptor will override the data in the AxiosRequestConfig object with the configuration in the destination before starting the request.

Let’s take the same example as before. Let’s post a new Book to the service.

const {SapCfAxios} = require('sap-cf-axios');
const destinationName = "MyDestinationNameInCloudFoundry";
const axios = SapCfAxios(destinationName);

const response = await axios({
  method: "post",
  url: "/BookSet",
  data: {
    title: "Using Axios in SAP Cloud Foundry",
    author: "Joachim Van Praet"
  },
  headers: {
    "content-type": "application/json"
  }
})

As you can see we deleted the baseUrl and auth property from our call. These properties will be read from the destination and injected by the sap-cf-axios library. If it is an onpremise service the proxy settings will also be injected by the library.

Use it yourself

Let’s try this thing!

I created a nodejs service to run locally. You can find it in here: https://github.com/jowavp/show-request

just run:

npm install
npm run start

The service starts on port 3002. The response of the service is a representation of the request parameters that are sent to this service.

let’s try it with postman locally. This is the request:

POST /BookSet HTTP/1.1
Host: localhost:3002
Content-Type: application/json
Authorization: Basic am93YTpzMDBwZXJzM2NyZXQ=
User-Agent: PostmanRuntime/7.19.0
Accept: */*
Cache-Control: no-cache
Postman-Token: ab328757-526e-4ab3-9464-d454a9abd9a1,157664fa-0efa-41ea-a97a-f440e836eb81
Host: localhost:3002
Accept-Encoding: gzip, deflate
Content-Length: 89
Connection: keep-alive
cache-control: no-cache

{
    "title": "Using Axios in SAP Cloud Foundry",
    "author": "Joachim Van Praet"
}

The response of this request is the following:

{
    "method": "POST",
    "baseUrl": "",
    "uri": "/BookSet",
    "host": "localhost",
    "headers": {
        "content-type": "application/json",
        "authorization": "Basic am93YTpzMDBwZXJzM2NyZXQ=",
        "user-agent": "PostmanRuntime/7.19.0",
        "accept": "*/*",
        "cache-control": "no-cache",
        "postman-token": "ab328757-526e-4ab3-9464-d454a9abd9a1",
        "host": "localhost:3002",
        "accept-encoding": "gzip, deflate",
        "content-length": "89",
        "connection": "keep-alive"
    },
    "body": {
        "title": "Using Axios in SAP Cloud Foundry",
        "author": "Joachim Van Praet"
    }
}

nothing fancy here because there is nothing happening with our request. It is send from postman to the service, so what we put in postman we will see in the response of our service. But things will get interesting when we use sap-cf-axios in cloud foundry to call our local service. First we have to configure the service in cloud connector so we can access it in cloud foundry.

Then we define a destination in cloud foundry that points to our local service using it’s virtual hostname.

When this configuration is done. We deploy a simple nodejs service that will call this destination with sap-cf-axios. https://github.com/jowavp/sap-cf-axios-example

const express = require('express');
const app = express();
const {SapCfAxios} = require('sap-cf-axios');
// the destination that we want to use is named 'DUMMY'
const axios = SapCfAxios('DUMMY');

app.use(express.json());

const handleRequest = async (req, res) => {
    try {
        
        const response = await axios({
            method: 'POST',
            url: '/BookSet',
            data: {
                title: "Using Axios in SAP Cloud Foundry",
                author: "Joachim Van Praet"
            },
            headers: {
                "content-type": "application/json"
        /* if you are using an oauth2* authentication type or principal propagation
        *  for your destination
        *  please add your JWT token in the authorization header of this request
        **/
        // authorization: <user JWT Token> 
        
            }
        });
        res.send(response.data);
    } catch (error) {
        res.reject(error);
    }
}

app.all('*', handleRequest);

const port = process.env.PORT || 3000;;
app.listen(port, function () {
  console.log('myapp listening on port ' + port);
});

you can deploy this application by running: (you need cli & mbt installed)

npm install
npm run build
npm run deploy

Once this application is deployed, we can open the url of the approuter to test it.

When we now look to the response. we see:

{
	"method": "POST",
	"baseUrl": "",
	"uri": "/BookSet",
	"host": "localhost",
	"headers": {
		"accept": "application/json, text/plain, */*",
		"content-type": "application/json",
		"authorization": "Basic am9hY2hpbTpzMDBwZXJzM2NyZXQ=",
		"sap-connectivity-scc-location_id": "my_cloud_connector_location_id",
		"user-agent": "axios/0.19.0",
		"content-length": "73",
		"connection": "close",
		"host": "localhost:3002",
		"x-forwarded-host": "dummy:3000"
	},
	"body": {
		"title": "Using Axios in SAP Cloud Foundry",
		"author": "Joachim Van Praet"
	}
}

As you can see sap-cf-axios added the authorization header and handled the proxy configuration to call the service on our local environment.

So, I hope this sap-cf-axios will make your life easier in consuming destinations in SAP cloud foundry. There are still some features missing that I will add when I need them. 🙂

 

kr,

Joachim

 

24 Comments
You must be Logged on to comment or reply to a post.
  • Hey Joachim!

    This is a great article and it covers exactly some of the issues we’re facing in the project my colleagues and I are working at the moment, so it couldn’t be more timely. So thanks for that.

    We’re really looking forward for the authentication principal propagation to on-premise systems feature, as it would help us with our use case.

    Best wishes,

    Jordi

    • Thanks Jordi!

      In the meanwhile I updated the library (v0.0.14) and it should be able to consume destinations with principal propagation. You just have to forward your current logon token to the request in the authorization header:

      const express = require('express');
      const app = express();
      const { SapCfAxios } = require('sap-cf-axios');
      
      const passport = require('passport');
      const xsenv = require('@sap/xsenv');
      const JWTStrategy = require('@sap/xssec').JWTStrategy;
      
      const onpremiseDest = SapCfAxios('GW');
      const services = xsenv.getServices({ uaa:'sca_uaa' });
      
      app.use(express.json());
      
      passport.use(new JWTStrategy(services.uaa));
      app.use(passport.initialize());
      app.use(passport.authenticate('JWT', { session: false }));
      
      const handleRequest = async (req, res) => {
          // do the dummy request
          const authorization = req.headers.authorization;
          
          try {
              const response = await onpremiseDest({
                  method: 'GET',
                  url: '/iwfnd/catalogservice/?$format=json',
                  params: {
                      "$format": 'json'
                  },
                  headers: {
                      "content-type": "application/json",
                      authorization  
                  }
              });
              res.send(response.data);
          } catch (error) {
              res.status(400).send(error);
          }
      }
      
      app.all('*', handleRequest);
      
      const port = process.env.PORT || 3000;;
      app.listen(port, function () {
          console.log('myapp listening on port ' + port);
      });

      To allow principal propagation the user needs the authorization scope “uaa.user”

      another remark: the forwarded JWT token does not contain the properties suggested (mail, name, display_name and login_name). When you configure the client certificate template, you have to change them manually with properties from the forwarded JWT token (e.g. user_name, email)

      kr,

      Joachim

      /
  • Hi Joachim,

    Thank you for creating the library . It is very useful. I am facing an issue in consuming the destination with OAuth2 credentials. It doesn’t work the same way. I am getting below error . Could you please help.

    const authorization = req.headers.authorization;
    const response = await brf ({
    				
       method: "post",
       url: "/rules-service/rest/v1/rule-services/java/rulesCWF/getCWFRules",
       data:  JSON.stringify(payload),
       headers: {
    		  "content-type": "application/json" ,
    		   authorization
    	    }
    	 })
      console.log(response)	
        		 
    			 

    Error

    Error: Empty Body with status 302 and message Found comes from the OAuth Service!
    at Object.<anonymous> (/home/vcap/app/node_modules/sap-cf-axios/dist/configEnhancer.js:19:24)
    at Generator.next (<anonymous>)
    at /home/vcap/app/node_modules/sap-cf-axios/dist/configEnhancer.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (/home/vcap/app/node_modules/sap-cf-axios/dist/configEnhancer.js:4:12)
    at Object.enhanceConfig [as default] (/home/vcap/app/node_modules/sap-cf-axios/dist/configEnhancer.js:14:12)
    at /home/vcap/app/node_modules/sap-cf-axios/dist/index.js:63:44
    at Generator.next (<anonymous>)
    at fulfilled (/home/vcap/app/node_modules/sap-cf-axios/dist/index.js:5:58)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:189:7)
  • Hi Manikandan Kannan

    Please try again with version 0.1.1 of the library. I was expecting a bearer token from the destination service for this authentication type, but it wasn’t there. I implemented it manually.

    For sap-cf-axios only an authorization header is needed if you try to authenticate with the current user. If you use client credential flow, you don’t need the authorization header.

    const response = await brf ({
    				
       method: "post",
       url: "/rules-service/rest/v1/rule-services/java/rulesCWF/getCWFRules",
       data:  JSON.stringify(payload),
       headers: {
    		  "content-type": "application/json"
    	    }
    	 })
      console.log(response.data)

    kr,

    Joachim

      • Hi Manikandan Kannan ,

        Are you shure you are using the latest version of the library?

        I’ve tested it here on my account with an OAuth2ClientCredentials destination to the Business Rules Service on a NEO account and it works like a charm!

        please double check the library version in your package.json.
        If it is possible to share your code in a git repo I can take a quick look at it.

        kr,

        Joachim

        • Hi Joachim,

           

          Yes I am using the library 0.1.1.  I dont have the git code only for the OAuth2 . If the below code is correct . I can try in local separately and provide you. But this is the snippet of the complete code.

          Please let me know if anything wrong.

           

          Package.json

          “sap-cf-axios”:”^0.1.1″

           

          Handlers:

          const basketFunction = async (req) => {
            
          	try {
                  
                         const payload = {};
          		
          		payload.__type__       = 'inputCWF';
          		payload.Proposed       = 12;
          		payload.Stretch	       = 12;
          		payload.Floor          = 14;
          		payload.Achieved       = 12;
          		payload.SubFloor       = 16; 
          		
          		const response = await brf({
          			method: "post",
          			url: "/rules-service/rest/v1/rule-services/java/rulesCWF/getCWFRules",
          			headers: {
          			"content-type": "application/json"  
          		  }
          		})
           
          	    console.dir(response);
          		return response.data;
          	} catch (error) {
          		console.log(error)
          	}
          };
          
          
          this.on('basket', basketFunction);   
          
          

           

          service.cds

          action basket() returns basketobj ; 
          • you are not sending your payload with your request …

            Can you share the log of the application and the value of response after the request?

            const response = await brf({
                            method: "post",
                            url: "/rules-service/rest/v1/rule-services/java/rulesCWF/getCWFRules",
                            headers: {
                                "content-type": "application/json"  
                            },
                            data: payload
                        })

            I retested my application here and it is really working 🙂

            Are you sure your destination is configured correctly?

            kr,

            Joachim

  • Hi Joachim ,

     

    I am using the library for long time now,it was working fine till yesterday and it is giving the new issue from today. There was no change in the configuration but not sure why its suddenly giving this error.

    Tried to debug but not able to find any issue because there was no configuration change.

    Kindly please help.

    Version I am using .

    “sap-cf-axios”: “0.1.3”

    Thanks

    /
    • Hi Manikandan Kannan ,

      Seems there is something wrong with the csrf protection handling.

      have you added xsrfHeaderName: x-csrf-token” in the request?
      The csrf handling is done with the options method.
      If the OPTIONS call is not available you have to handle the csrf token yourself.
      To disable the csrf protection set

      xsrfHeaderName:null

      I’m quite busy at the moment, but I created a version 0.1.5 that may fix your problem.

      Can you try?        

      kr,
      Joachim
  • H Joachim,

     

    We are using this library as the base to get the data for the project and we are stuck now  🙁

    I changed it to 0.1.5 but still same issue.

    Regquest

    	const response = await sapcf({
    			method: "get",
    			url: "/BPM_ODATA_HANA_V1_SRV/PROJECTCollection",
    			headers: {
    				"content-type": "application/json" 
    
    			},
    			 xsrfHeaderName: "x-csrf-token"
    		});
    • Hi Joachim,

       

      Yes, as you mentioned OPTIONS method is not available to call the ECC to fetch CSRF token . You suggesting to use it by our self to fetch instead using library?

      It was working fine till yesterday using 0.1.3 with the same library. 

      I also see axios has published a new version of library 0.19.1 yesterday .

      const response = await sapcf({
      			method: "get",
      			url: "/BPM_ODATA_HANA_V1_SRV/PROJECTCollection",
      			headers: {
      				"content-type": "application/json",
      				"X-CSRF-Token": 'Fetch'
      			}
      		});
      • I did not change anything on this. The X-CSRF-Token was using OPTIONS call from the beginning.
        I created a version 0.1.7 where you can configure the csrf method in the constructor, I hope this helps …

        const { SapCfAxios } = require('sap-cf-axios');
        const onpremiseDest = SapCfAxios('GW', null, {method: 'GET', url: '/'});

        The last parameter defines the http method used for the CSRF token.

        If this is not working, please provide me a code snippet, then I will try to create a similar scenario to test.

         

        • Same Error and I am not able to use it  🙁 I think its not only the method which we pass ,since there is a change in the version of the AXIOS library it is throwing error ,because it is not fetching the CSRF token. Axios verison 0.19.0 was fine. Yesterday they updated with 0.19.1 and from then it stopped working.

  • Hi Joachim,

     

    It dint work either. Yes it was option method before too,but it was working fine because it was going GET(whatever mentioned  in the request)  to ECC .

    This version 0.1.6 goes as GET method but still getting the error.

    Code Snippet

    const csr = SapCfAxios("destbasic");
    const onpremiseDest = SapCfAxios('destbasic', null, 'GET');
    
    const fetchCSRF = async(req) => {
    
    	try {
    
    		const response = await onpremiseDest({
    			method: "get",
    			url: "/BPM_ODATA_HANA_V1_SRV/PROJECTCollection",
    			headers: {
    				"content-type": "application/json" 
    
    			},
    			 xsrfHeaderName: "x-csrf-token"
    		});
    
    		return response.headers;
    	} catch (error) {
    		console.log(error)
    	}
    };

     

     

  • Hi Joachim,

     

    Thank you for all the update to the library as we are closely following it. It is very much useful . We were using the library with the basic authentication and everything was working fine. Now we switched to the principal propagation and it is giving us the error. We use cloud connector to connect to the ECC and we are getting the error as

    { error: [32m’unauthorized'[39m,
    2020-03-09T20:44:24.072+0000 [APP/PROC/WEB/0] OUT error_description:
    2020-03-09T20:44:24.072+0000 [APP/PROC/WEB/0] OUT [32m’An Authentication object was not found in the SecurityContext'[39m } },

    The way I used the library is .

    module.exports = (srv) => {
      
    const SapCfAxios = require('sap-cf-axios').default;
    const sps= SapCfAxios("sps-qa");
    
     
      const { whoami } = srv.entities
     
      srv.on('whoami', async(req)=>{
      
        try {
           console.log('Request...........'+JSON.stringify(req.attr.token))
           
             let authorization=req.attr.token;
    
          const response = await sps({
            method: "get",
            url: "/SPS/BPM_ODATA_HANA_V1_SRV/GetUserLogin?SYUNAME=''&$format=json",
            headers: {
              "content-type": "application/json",
              authorization
          }
          })
       
        // console.log('Resultss........'+JSON.stringify(response))
         return results;
        } catch (error) {
          console.log(error)
        }
      })   
    
    }
    
  • Hi Joachim,

     

    When i try this out and push it to cloud foundry I get an error that says

     

    2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR const axios = SapCfAxios(destinationName);
    2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR ^
    2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR TypeError: SapCfAxios is not a function

     

    anything changed recently?

     

  • Some of my observations.

     

    declaration of SapCfAxios ->

    const SapCfAxios = require('sap-cf-axios').default;
    const destination = SapCfAxios('shp');
    
    
    const response = await destination({
                method: 'GET',
                url: '/TOOLS/Services/KPI/KpiService.xsjs',
                params: {
                    "ACTION": 'RA_OVERVIEW'
                },
                headers: {
                    "content-type": "application/json",
                    "Authorization": `${req.headers.authorization}`
                }
            });

    App router bearer token is available at req.headers.authorization (this is because I have set forwardAuthToken is true in destinations from AppRouter to Node.js app)

    Using this I can make onPremise XSJS or OData calls.

    My concern is with XSUAA having breaking changes every so often i think it is better to do an OAuth call with the UAA to get the token and then add it to the authorization header rather than getting the token from request headers.

    All in all this is a great library and makes onPremise data consumption very very simple.

    Thanks a lot Joachim.

     

    Regards,

    Abhi

  • Hello  Joachim Van Praet ,

    I try to connect my nodeJS application to my system backend , i prepare the service on the backend, the cloud connector , the app-router , the nodejs application(with  services uaa, destination , connectivity) and i create the destination on scp , i deploy my application  on scp but when i execute i find always Gatewaytimed out. and i tested the backend service with a simple approuter and it work correctly and i find the result on the browser.

    i try to do this with axios and with sap-cf-axios

    Can you help me pleaseeeeeee

    1/ this is the connection of cloud connector on scp

    2/ this is my destination on scp

     

    3/ this is the service tested with app-router

    4/this the function i prepared with axios

    5/ this is my function with sap-cf-axios

  • Hi Joachim Van Praet ,

    this package is awesome and made my life much easier today.

    I am building a decoupled integration using SAP Enterprise Messaging. Now I am able to receive messages on a queue in the app, transform it and post it to make SAP Marketing onPrem System using your npm module.

    Cheers

    Christian